往期文章:
往期文章:
- 重学webpack4之原理分析
- 重学webpack4之基础篇
- 重学webpack4之loader开发
- 重学webpack4之plugin开发
- webpack插件开发之秒开缓存插件
- 重学webpack4之打包库和组件
- 重学webpack4之构建速度提升和体积优化
安装
- cnpm i webpack webpack-cli
- 模块局部安装会在 node_modules/.bin/webpack 目录创建软链接
基础
entry
- 依赖入口
// 单入口,SPA
entry: 'xx/xx.js'
// 多入口 MPA
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
}
output
- 指定打包后的输出
output: {
path: path.resoluve(__diranme,'dist')
filename: '[name].js' // 单入口可以写死文件名,多入口一定要使用占位符[name],来自动生成多个文件
// filename: '[name].[chunkhash:5]]js'
// filename: '[name].[hash]js'
}
Loaders
- wepback开箱即用只支持JS和JSON两种文件类型,通过Loader去支持其他文件类型并把他们转化成有效的模块,并且可以添加到依赖图中
- 本身是一个函数,接受源文件作为参数,返回转换的结果
- 例如: babel/ts-loader、css/less/scss-loader、url/file-loader、raw-loader(将.txt文件以字符串的形式导入)、thread-loader(多进程打包js和css)
module: {
rules: [
{test: /.txt$/, use: 'raw-loader'},
{test: /.css$/, use: [
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
}
]}
]
}
解析ES6
- 使用babel-loader,babel的配置文件.babelrc
- 安装 babel-loader、@babel/core @babel/preset-env
// webpack.config
module: {
rules: [
{test: /.js?x$/, use: 'babel-loader', exclude: /node_modules/}
]
}
// .babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
// 各种插件
"@babel/propsoal-class-properties"
]
}
css/less/scss-loader
- css-loader用于加载.css文件,并且转换成 common 对象
- style-loader 将样式通过 style 标签,插入到 head 中
// use: [loader1,loader2,loader3],loader的处理顺序是 3>2>1,从后往前
module: {
rules: [
{
test: /.s?css$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader
{
loader: 'css-loader',
options: {
importLoaders: 1,
// css模块化使用
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader',
'sass-loader'
],
include: [],
exclude: [
Root('src/components')
]
},
]
}
url/file-loader
- 用于处理文件,图片、字体、多媒体
- url-loader 实现较小的图片转成base64,插入到代码中,当超过限制的limit后,会自动降级到file-loader
{
test: /\.(png|jpg|jpeg|gif|eot|woff|woff2|ttf|svg|otf)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10 * 1024, // 10k
name: isDev ? 'images/[name].[ext]' : 'images/[name].[hash.[ext]',
publicPath: idDev ? '/' : 'cdn地址',
},
},
// prduction,用于图片压缩
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true
}
}
]
},
postcss-loader
- 预处理器, autoprefixer(需要安装)
rules: [
{test: /.css$/, use: [
{
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => {
require('autoprefixer')({
browsers: ['last 2 version', '>1%', 'ios7']
})
}
}
},
'sass-loader'
}
]
}
或者将postcss-loader的options放在根目录的postcss.config.js中
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
'less-loader',
]
}
]
// postcss.config.js 和 package.json(或者.browserslistrc,推荐使用.browserslistrc)
// 安装postcss-preset-env,autoprefixer
module.exports = {
loader: 'postcss-loader',
plugins: {
'postcss-preset-env': {
stage: 0,
features: {
'nesting-rules': true,
'autoprefixer': { grid: true },
''
}
}
}
};
// package.json
"browserslist": [
"last 2 version",
">1%",
"iOS >= 7"
]
// .browserslistrc
last 2 version
>1%
iOS >= 7
px自动转rem(两种方式)
- 方式一:px2rem-loader与lib-flexible,页面渲染时计算根元素的font-size,推荐使用:
-
- cnpm i -D px2rem-loader
-
- cnpm i -S lib-flexible (不推荐)将lib-flexible代码拷贝到html>head>script中 使用raw-loader内联lib-flexible
rules: [
{
test: /.css$/,
use: [
{
'style-loader',
'css-loader',
'postcss-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // 一个rem等于多少px
remPrecision: 8 // px转换成rem的小数位
}
}
'sass-loader',
}
]
}
- 方式二,使用postcss-loader与postcss-px2rem-exclude
module.exports = {
loader: 'postcss-loader',
plugins: {
'postcss-preset-env': {
stage: 0,
features: {
'nesting-rules': true,
'autoprefixer': { grid: true },
}
},
'postcss-px2rem-exclude': {
remUnit: 200,
exclude: /node_modules/i
}
}
};
plugins
- 插件用于bundle文件的优化,资源管理和环境变量注入,作用于整个构建过程
plugins: [
new HtmlWpeckPlugin({})
]
webpack-dev-server
- webpack-dev-server,开启本地服务器,监听文件变化后,热更新页面
- 不刷新浏览器而是热更新,不输出文件,而是放在内存中
- 配合 new.webpack.HotModuleReplacementPlugin() 或 react-hot-loader 插件使用
// package.json
webpack-dev-server mode=development -open
// config
module.exports = {
devServer: {
host: '0.0.0.0',
compress: true,
port: '3000',
contentBase: join(__dirname, '../dist'),//监听的目录,用于刷新浏览器
hot: true,
overlay: {
errors: true,
warnings: true
},
disableHostCheck: true,
publicPath: '/', // 设置时,要与output.publicPath保持一致
historyApiFallback: true,
// historyApiFallback: {
// rewrites: [from: /.*/, to: path.posix.join('/', // 'index.html')],
//}
proxy: {
'/api': 'http://localhost:8081',
}
//proxy: {
// '/api/*': {
// target: 'https://xxx.com',
// changeOrigin: true,
// secure: false,
// headers: {},
// onProxyReq: function(proxyReq, req, res) {
// proxyReq.setHeader('origin', 'xxx.com');
// proxyReq.setHeader('referer', 'xxx.com');
// proxyReq.setHeader('cookie', 'xxxxx');
// },
// onProxyRes: function(proxyRes, req, res) {
// const cookies = proxyRes.header['set-cookie'];
// cookies && buildCookie(cookies)
// }
// }
// }
},
}
webpack-dev-middleware
- 将webpack输出文件传输给服务器,适用于灵活的定制场景
const express = requrie('express')
const webpack = require('webpack')
const webpackDevMiddleware = requrie('webpack-dev-middleware')
const app = express()
const config = require('./webpack.config.js')
const compiler = webpack(config)
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}))
app.listen(3000)
热更新原理
mini-css-extract-plugin和optimize-css-assets-webpack-plugin
- 提取css,建议使用contenthash
module: {
rules: [
{
test: /.s?css$/,
use: [
isDev ? 'style-loader' : iniCssExtractPlugin.loader
{
loader: 'css-loader',
options: {
importLoaders: 1,
// css模块化使用
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader',
'sass-loader'
]
},
]
},
plugins: [
// 提取css
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash:5].css',
}),
// 压缩css
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require("cssnano"),//需要安装cssnano
cssProcessorPluginOptions: {
preset: [
'default',
{
discardComments: {
removeAll: true
}
}
]
},
canPrint: true
}),
]
html-webpack-plugin
plugins: [
new HtmlWebpackPlugin({
// 自定义参数title传递到html中
// html中使用<title><%= htmlWebpackPlugin.options.title %></title>
// <script>let number = <%= htmlWebpackPlugin.options.number %><script>
number: 1,
title: '京程一灯CRM系统',
filename: 'index.html',
// chunks: ['index'] //用于多页面,使用哪些chunk
template: resolve(__dirname, '/src/index.html'),
minify: {
minifyJS: true,
minifyCSS: true,
removeComments: true,
collapseWhitespace: true,
preserveLineBreak: false,
removeAttributeQuotes: true,
removeComments: false
}
}),
]
clean-webpack-plugin 或者使用 rimraf dist
plugins: [
new CleanWebpackPlugin()
]
mode
- Mode 用来指定当前构建环境 development、production 和 none
- 设置 mode 可以使用 wepack内置函数,内部自动开启一些配置项,默认值为 production
内置功能
development:process.env.NODE_ENV为development,开启NamedChunksPlugin 和 NameModulesPlugin
这两个插件用于热更新,控制台打印路径
prodution:process.env.NODE_ENV为prodution.开启 FlagDependencyUsagePlugin、ModuleConcatenationPlugin、NoEmitOnErrorsPlugin,OccurrentceOrderPlugin、SideEffectsFlagPlugin等
none:不开启任何优化选项
watch
- 文件监听可以在webpack命令后加上 --watch 参数,或在webpack.config中设置watch:true
- 原理:轮询判断文件的最后编辑时间是否变化
module.exports = {
// 默认false,不开启
watch: true,
// 只有开启时,watchOptions才有意义
watchOptions: {
// 忽略,支持正则
ignored: /node_modules/,
// 监听到变化后等300ms再执行,默认300ms
aggregateTimeout: 300,
// 怕乱文件是否变化是通过不停询问系统指定文件有没变化实现的,默认每秒1000次
poll: 1000
}
}
文件指纹
- 打包后输出文件名后缀,也就是hash值
- hash:和整个项目构建相关,只要项目中有一个文件修改,整个项目中的文件hash都会修改成统一的一个
- chunkhash:和webpck打包的chunk有关,不同的entry会生成不同的chunkhash值(适用于js文件)
- contenthash:根据文件内容定义hash,文件内容不变,则contenthash不变,用于批量更新(适用于css文件)
资源内联
- 页面框架的初始化,比如flexible
- 上报相关打点
- css内联避免页面闪动
// raw-loader@0.5.1 内联html片段,在template中弄
<!-- 内联html -->
<%= require('raw-loader!./meta.html') %>
// raw-loader内联js
<!-- 初始化脚本,例如flexible -->
<script><%= require('raw-loader!babel-loader!../node_modules/lib-flexible/flexible.js') %></script>
css内联合
- 方式一:style-loader
module: {
rules: [
{
test: /.s?css$/,
use: [
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag', // 将所有style标签合并成一个
}
}
'css-loader'
'postcss-loader',
'sass-loader'
]
},
]
},
- 方式二:html-inline-css-webpack-plugin 首先使用 mini-css-extract-plugin(而非 style-loader)将 css 提取打包成一个独立的 css chunk 文件 然后使用 html-webpack-plugin 生成 html 页面 最后使用 html-inline-css-webpack-plugin 读取打包好的 css chunk 内容注入到页面,原本 html-webpack-plugin 只会引入 css 资源地址,现在实现了 css 内联
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: "[id].css"
}),
new HtmlWebpackPlugin(),
new HTMLInlineCSSWebpackPlugin(),
],
}
style-loader vs html-inline-css-webpack-plugin
- style-loader是css-in-js,需要加载js后才能写入到style中,有一定的延迟性
- html-inline-css-webpack-plugin是将css提取出来,再写入到html中,html网页源代码中已经内联好css了,没有延迟性了
- 好不好,谁用谁知道
请求成面:减少HTTP网络请求数
- 小图片或者字体内联(url-loader)
多页面应用 MPA
- 每一次页面跳转,后台都会返回一个新的html,多页应用
- 优势:SEO友好、每个页面是解耦的
- 每个页面对应一个entry,一个html-webpack-plugin,(这种太麻烦了,每次新增都需要再配置一次)
- 解决方案:
// 例如 ./src/index/index.js 与 ./src/search/index.js
// path: './src/*/index.js'
const setMPA = filenames => {
const entry = {}, htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, filenames))
for(let item of entryFiles){
// (/\/([a-z\_\$]+)\/index.js$/)
const match = item.match(/src\(.*)/index\.js$/)
const pageName = match && match[1];
entry[pageName] = item
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: `src/${pageName}/index.html`,
filename: `${pageName}.html`,
chunks: ["runtime", "common", pageName],
minify: {
// ..
}
})
)
}
return {
entry,
htmlWebpackPlugins
}
}
entry: entry
plugin: [//.....].concat(htmlWebpackPlugins)
source-map
-
通过source-map 定位到源代码
-
开发环境开启,线上环境关闭
-
eval:使用eval包裹模块代码
-
cheap:不包含列信息
-
inline:将.map作为DataURI嵌入,不单独生成.map文件
-
module:包含loader的source
-
开发环境:建议使用
首先在源代码的列信息是没有意义的,只要有行信息就能完整的建立打包前后代码之间的依赖关系。因此不管是开发环境还是生产环境,我们都会选择增加cheap基本类型来忽略模块打包前后的列信息关联。
其次,不管在生产环境还是开发环境,我们都需要定位debug到最最原始的资源,比如定位错误到jsx,coffeeScript的原始代码处,而不是编译成js的代码处,因此,不能忽略module属性
再次我们希望通过生成.map文件的形式,因此要增加source-map属性
module.expors = {
// 开发,因为eval的rebuild速度快,因此我们可以在本地环境中增加eval属性
devtool: 'cheap-module-eval-source-map'
// 生产
devtool: 'cheap-module-source-map'
}
代码拆分
基础库分离
将react、react-dom基础包通过cdn引入,不打入到bundle中
- 方式一: externals
// 1
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'react-router-dom': 'ReactRouterDOM',
mobx: 'mobx'
},
}
2. 在html模版中 script标签引入对应的cdn地址
- 方式二:html-webpack-externals-plugin(推荐使用)
1.在html模版中 script标签引入对应的cdn地址
2.plugins: [
new HtmlWebpackPlugin(),
new htmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: react的cdn地址,
global: 'React'
},
{
module: 'react-dom',
entry: react-dom的cdn地址,
global: 'ReactDOM'
},
{
module: 'react-router-dom',
entry: react-router-dom的cdn地址,
global: 'ReactRouterDOM'
},
// ...
]
})
]
- 方式三:webpack4 替代 CommonsChunckPlugin插件
module.exports = {
optimization: {
minimize: true,
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
chunks: 'async', // async异步引入库进行分离(默认),initial同步引入库分离,all所有引入库分离
minSize: 30000, // 抽离公共包最小的大小
maxSize: 0,
minChunks: 1, // 最小使用的次数
maxAsyncRequests: 5,
maxInitialRequests: 3,
name: true,
cacheGroups: {
// 提取基础库,不使用CDN的方式
//commons: {
// test: /(react|react-dom|react-router-dom)/,
// name: "vendors",
// chunks: 'all'
//},
// 提取公共js
commons: {
chunks: "all", // initial
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
name: "commons"
},
// vendors: {
// test: /[\\/]node_modules[\\/]/,
// priority: -10
// }
// 合并所有css
// styles: {
// name: 'style',
// test: /\.(css|scss)$/,
// chunks: 'all',
// minChunks: 1,
// enforce: true
// }
}
}
},
}
tree-shaking(静态分析,不是动态分析)
- 代码不会被执行到,就不会打包到bound.js
- 必须使用ES6的语法(import、export)才支持tree-shaking,commonjs方式不支持,
- webpck默认支持,在.babelrc里面设置 modules: false即可,同时mode=production默认开启
- 原理:利用ES6模块的特点:
-
- 只能作为模块顶层的语句出现
-
- import的模块只能是字符串常量 export function() {}
-
- import binding 是 immutable 的 代码擦除: uglify阶段删除无用代码
Scope Hoisting
- 大量函数闭包包裹代码,导致体积增大(模块越多越明显)
- 运行代码时创建的函数作用域变多,内存开销变大
- 被webpack转换后的模块会带上一层包裹,import会被转换成__webpack_require__
(function(module, __webpack_exports__,__webpack_require__){
__webpack_require__.r(__webpack_exports__);
})()
-
scope hoisting原理:将所有模块的代码按照引用顺序放在一个函数作用域中,然后适当的重命名一些变量以防止变量名冲突
-
对比,通过scope hoisting 可以减少函数声明代码和内存开销
-
开启scop hoisting
-
- webpack4 mode 为 production默认开启,必须是ES6语法,commonJS不支持
-
- webpack3 增加插件 new webpack.optimize.ModuleConcatenationPlugin()
代码分割
- splitChunck
- 动态引用
-
- 适用场景:抽离相同代码到一个共享块
-
- 脚本懒加载,使得初始下载代码更小
- 懒加载JS脚本方式
-
- CommonJS: require.ensure
-
- ES6: 动态import(需要babel支持,@babel/plugin-syntax-dynamic-import)
// 配置.babelrc
"plugins": [
["@babel/plugin-syntax-dynamic-import"],
]
dist代码通过window['webpackJsonp']来获取对应脚本
ESlint
//.eslint.js
/ 区分生产环境、开发环境
const _mode = process.env.NODE_ENV || 'production';
module.exports = {
"env": {
"browser": true,
"es6": true,
"node": true,
},
"globals": {
"$": true,
"process": true,
"dirname": true,
},
"parser": "babel-eslint",
"extends": "eslint:recommended",
"parserOptions": {
"ecmaFeatures": {
"jsx": true,
"legacyDecorators": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"no-console": "off",
"no-debugger": _mode==='development' ? 0 : 2,
"no-alert": _mode==='development' ? 0 : 2,
// "no-multi-spaces": "error",
"no-unused-vars": "off", // react中不适用
"no-constant-condition": "off",
"no-fallthrough": "off",
// "keyword-spacing": ["error", { "before": true} ], // 不生效,先注释
// "indent": [
// "error",
// 2
// ],
"linebreak-style": [
"error",
"unix"
],
// "quotes": [
// "error",
// "single"
// ],
"semi": [0],
"no-unexpected-multiline": 0,
"no-class-assign": 0,
}
};
检查eslint
- 方式一: 安装husky,增加npm script,适合老项目
"scripts": {
//"precommit": "eslint --ext .js --ext .jsx src/",
"precommit": "eslint lint-staged", // 增量检查修改的文件
},
"lint-staged": {
//"src/**/*.js": [
// "eslint --ext .js --ext .jsx",
// "git add"
//]
"linters": {
"*.[js,scss]": ["eslint --fix", "git add"]
}
}
- 方式二:webpack与eslint结合,新项目
rules: [
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: ['babel-loader', 'eslint-loader']
}
]
其实新项目中,可以将两种方式同时使用
优化命令行日志
- 统计信息 stats,webpack属性,这种方式不好
-
- error-only:值发生错误时输出
-
- minimal:只在发生错误或有新的编译时输出
-
- none:没有输出
-
- normal:标准输出,默认
-
- verbose:全部输出
// development
devServer: {
// .....
stats: 'errors-only'
}
// production
module.exports = {
stats: 'errors-only'
}
- stats结合friendly-errors-webpack-plugin(推荐)
plugins: [
new FriendlyErrorsPlugin()
],
stats: 'errors-only'
构建异常和中断处理
- wepback4之前的版本构建失败不会跑出错误码
- node中的process.exit规范
-
- 0 表示成功完成,回调函数中,err 为 null
-
- 非0 表示执行失败,回调函数中,err 不为空,err.code就是传给exit的数字
- 主动捕获错误,并处理构建错误
-
- 写个插件,compiler 在每次构建结束后会出发done这个hook
plugins: [
function() {
// webpack3 this.plugin('done', (stats) => {})
// webpack4
this.hooks.done.tap('done', (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) {
console.log('build error');
// dosomething
process.exit(1);
}
})
}
]
❤️ 加入我们
字节跳动 · 幸福里团队
Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者
期待您的加入,一起用技术改变生活!!!