ravencrown / play-webpack Goto Github PK
View Code? Open in Web Editor NEW全面解析 webpack 核心功能和优化策略,源码分析
全面解析 webpack 核心功能和优化策略,源码分析
配置
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
效果
每次构建的时候webpack不会自动清理目录,造成构建的输出目录 output 文件越来越多
所以可以在每次构建之前,删除下目录下的文件,可以通过 npm script 清理构建目录
$ rm -rf ./dist && webpack
$ rimraf ./dist && webpack
也可以采用 webpack 提供的 clean-webpack-plugin
,默认会删除 output 指定的输出目录。使用插件可以避免每次构建前手动删除dist
目录
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name][chunkhash:8].js'
},
plugins:[
new CleanWebpackPlugin()
]
};
url-loader 即可以处理图片,也可以处理字体,可以设置较小资源base64
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240
}
}
]
}
]
}
};
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
});
效果
概念:1个模块可能有多个方法,只要其中的某个方法使用到了,则整个文件都会被打包到bundle 里面去,tree shaking 就是只把只用到的方法打入bundle,没用到的方法会在 ugligy 阶段被擦除掉
使用:webpack 默认支持,在 .babelrc 里设置 modules:false 即可,mode=production 的情况下默认开启
要求:必须是 ES6 的语法,CJS(common js) 的方式不支持
DCE(Dead code elimination)原理 - tree-shaking 用到了DCE原理
if (false) {
console.log("这段代码永远不会被执行")
}
Tree-shaking 原理
利用 ES6 模块的特点
代码擦除:tree-shaking会对无用代码作比较,然后在 uglify 阶段删除无用代码。
使用两种loader
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.less$/, // less-loader 用于解析 less
use: [
'style-loader',
'css-loader',
'less-loader'
]
},
]
}
};
entry 是webpack的入口,支持单页面和多页面的形式
// 单页面, entry是一个字符串
module.exports = {
entry: './path/to/my/entry/file.js'
};
// 多页面,entry 是一个对象
module.exports = {
entry: {
app: './src/app.js',
adminApp: './src/adminApp.js'
}
};
output 用来告诉webpack如何将编译后的文件输出到磁盘
// 单入口配置
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
filename: 'bundle.js’,
path: __dirname + '/dist'
}
};
// 多入口配置
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js', path: __dirname + '/dist'
}
};
Hash: 和整个项目的构建有关,只要项目的文件有修改,整个项目构建的 hash 值就会更改
ChunkHash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值
Contenthash:根据文件的内容来定义 hash,文件内容不变,则 contenthash 不变
JS 的文件指纹
设置 output 的 filename,使用 chunkhash
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name][chunkhash:8].js'
}
};
CSS 文件指纹
设置MiniCssExtractPlugin的filename,使用contenthash
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name][chunkhash:8].js'
},
plugins:[
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
})
]
};
设置 file-loader 的filename,使用【hash】
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
name: 'img/[name][hash:8].[ext]'
}
}
]
}
},
};
文件监听是在发现源码发生变化时,⾃动重新构建出新的输出文件。
webpack 开启监听模式,有两种⽅式:
使用
{
"name": "hello-webpack",
"version": "1.0.0",
"description": "Hello webpack",
"main": "index.js",
"scripts": {
"build": "webpack ",
"watch": "webpack --watch"
},
"keywords": [],
"author": "",
"license": "ISC"
}
原理监听分析:轮询判断⽂件的最后编辑时间是否变化,某个文件发生了变化,并不会立刻告诉监听者,⽽是先缓存起来,等 aggregateTimeout
{
"name": "hello-webpack",
"version": "1.0.0",
"description": "Hello webpack",
"main": "index.js",
"scripts": {
"build": "webpack ",
"dev": "webpack - dev - server--open"
},
"keywords": [],
"author": "",
"license": "ISC"
}
WDM 将 webpack 输出的文件传输给服务器器,适⽤用于灵活的定制场景
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('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, function () {
console.log('Example app listening on port 3000!\n');
});
source map 的作用是定位webpack打包之后的bundle文件的源码位置,科普请看这篇文章JavaScript Source Map 详解
开发环境开启,线上环境关闭:线上排查问题的时候可以将sourcemap 上传到错误监控系统
sourcemap 关键字
.map
文件.map
作为DataUrl 嵌入,不单独生成 .map
文件source map 类型
eval 效果
eval 把模块代码包裹住,并且结尾使用 sourceURL 指向模块代码的文件
source-map 效果
源文件和 .map
文件分开,源文件结尾有sourceMappingUrl 标志,如图
inline-source-map 效果
文件不分离,把sourcemap inline进入了源文件,源文件大小变大。
JS压缩:采用内置的 uglifyjs-webpack-plugin
CSS压缩
使用 optimize-css-assets-webpack-plugin,同时使用 cssnano
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
HTML 压缩
设置 html-webpack-plugin
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/search.html'),
filename: 'search.html',
chunks: ['search'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
file-loader ⽤用于处理理⽂文件
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /.(png|jpg|gif|jpeg)$/,
use: 'file-loader'
}
]
}
};
如何判断构建是否成功
echo $?
获取错误码webpack4 之前的版本构建失败不会抛出错误码(error code)
nodejs 中的 process.exit 规范
如何主动捕获并处理构建错误
// webpack 配置 plugin
plugins: [
function() {
this.hooks.done.tap('done', (stats) => {
if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1)
{
console.log('build error');
process.exit(1);
}
})
}
]
步骤1:配置.babelrc
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"@bable/proposal-class-properties"
]
}
步骤2:配置webpack loader
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader'
}
]
}
};
webpack 打包展示一堆日志,很多并不需要开发者关注
统计信息 stats
优化命令行的构建日志
使用 friendly-errors-webpack-plugin 插件
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
},
plugins: [
new FriendlyErrorsWebpackPlugin()
},
stats: 'errors-only' // 设置 stats
}
步骤1:配置.babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@bable/proposal-class-properties"
]
}
基础库分离:React示例
思路:将react、react-dom 基础包通过cdn引入,不打入bundle
方法:使用html-webpack-extends-plugin
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://11.url.cn/now/lib/16.2.0/react.min.js',
global: 'React',
},
{
module: 'react-dom',
entry: 'https://11.url.cn/now/lib/16.2.0/react-dom.min.js',
global: 'ReactDOM',
},
]
}),
]
使用 SplitChunksPlugin 进行公共脚本分离
webpack4 内置的,替代 CommonsChunkPlugin(webpack3使用的多) 插件,SplitChunksPlugin介绍请参考官方文档
chunks参数说明:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000, // 分离包体积的大小
maxSize: 0,
minChunks: 2, // 设置最小引用次数为2次
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
automaticNameMaxLength: 30,
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 匹配出需要分离的包
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
现象:构建后的代码存在大量的闭包代码
会导致什么问题?
webpack模块转换分析
如图
结论
进一步分析 webpack 的模块机制
分析以上代码
scope hoisting 原理
原理:将所有的模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
对比:通过 scope hoisting 可以减少函数声明代码和内存开销
scope hoisting 使用
可以参考官网对mode的描述,mode=production模式下自动开启FlagDependencyUsagePlugin , FlagIncludedChunksPlugin , ModuleConcatenationPlugin , NoEmitOnErrorsPlugin , OccurrenceOrderPlugin , SideEffectsFlagPlugin and TerserPlugin
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
},
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
};
}
行业里面优秀的 ESLint 规范实践
Airbnb: eslint-config-airbnb、 eslint-config-airbnb-base
腾讯
指定团队的 ESLint 规范
方案一:webpack 和 CI/CD 集成
本地开发阶段增加 precommit 钩子
$ npm install husky --save-dev
npm script
,通过 lint-staged
增量检查修改的文件"scripts": {
"precommit": "lint-staged"
},
"lint-staged": {
"linters": {
"*.{js,scss}": ["eslint --fix", "git add"]
}
}
方案二:webpack 和 ESLint 集成
使用 eslint-loader,构建时检查 JS 规范
module.exports = {
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [
"babel-loader",
"eslint-loader"
]
}
};
]
}
.eslintrc.js 文件配置
module.exports = {
"parser": "babel-eslint", // 解释器
"extends": "airbnb", // 继承airbnb规则
"env": { // 设置环境,当前设置了浏览器和node环境
"browser": true,
"node": true
},
"rules": {
"indent": ["error", 4]
}
};
CSS3属性为什么需要前缀?
因为:浏览器内涵版本不一样
webpack 的PostCSS配置如下
module: {
rules: [
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
browsers: ['last 2 version', '>1%', 'ios 7']
})
]
}
}
]
}
]
},
在webpack里,每个页面对应一个 entry
,一个html-webpack-plugin。
缺点:每次新增或删除页面都需要修改 webpack 配置。
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
}
};
解决方案1:动态获取 entry 和设置 html-webpack-plugin 数量
解决方案2:利用 glob.sync
entry: glob.sync(path.join(__dirname, './src/*/index.js'))
这两个解决方案的前提是把入口文件放到 /src/*/index.js 下
,所以的入口文件都叫index.js
,通过二级目录来区分。
module.exports = {
entry: {
index: './src/index/index.js',
search: './src/search/index.js'
}
};
如果使用glob
库的话,
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
Object.keys(entryFiles)
.map((index) => {
const entryFile = entryFiles[index];
// '/Users/cpselvis/my-project/src/index/index.js'
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFile;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
inlineSource: '.css$',
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: ['vendors', pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
);
});
return {
entry,
htmlWebpackPlugins
}
}
file-loader 也可以⽤于处理字体
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /.(woff|woff2|eot|ttf|otf)$/,
use: 'file-loader'
}
]
}
};
webpack 开箱即用只支持JS和JSON两种格式,通过loader去支持其他文件类型并且把他们转化为有效的模块,并且可以添加到依赖图中。
loader本身是一个函数,接受源文件为参数,返回转换的结果
常见的loader有
名称 | 描述 |
---|---|
bable-loader | 转换ES6/ES7等新特性语法 |
css-loader | 支持.css 文件的加载和解析 |
less-loader | 将less文件转换为css |
ts-loader | 将TS转换为JS |
file-loader | 进行图片、字体的打包 |
raw-loader | 将文件以字符串的形式导入 |
thread-loader | 多进程打包JS和CSS |
Loaders 的用法
// 配置示例
const path = require('path');
module.exports = {
output: {
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.txt$/, // test 指定匹配规则
use: 'raw-loader' // use 指定使用的 loader 名称
}]
}
};
插件用于bundle文件的优化,资源管理和环境变量注入,作用于整个构建过程
常见的plugins
Plugins 的用法
const path = require('path');
module.exports = {
output: {
filename: 'bundle.js'
},
plugins: [ // 放在plugins 数组中
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
资源内联的意义
代码层面
请求层面:减少HTTP网络请求数
raw-loader 内联 HTML
<script>${require('raw-loader!babel-loader!./meta.html')}</script>
raw-loader 内联 JS
<script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>
css内联1 - style-loader
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /.scss$/,
use: [
{
loader: 'style-loader',
options: [
insertAt: 'top', // 将样式插入到 <head>
singleton: 'true' // 将所有的 style 标签合成一个
]
},
'css-loader',
'sass-loader'
]
}
]
}
};
css内联2 - html-inline-css-webpack-plugin
首先先看下移动端的浏览器分辨率
px 自动装换成 rem可以使用 px2rem-loader,页面渲染时计算根元素的 font-size 值。也可以使用手淘的lib-flexible插件
webpack 配置如下
module: {
rules: [
{
test: /.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // 1rem = 75px
remPrecision: 8 // 小数点位数
}
}
]
}
]
},
Lib-flexible 的源码, 短短不到一百行
动态的计算根元素的rem单位。
(function flexible (window, document) {
var docEl = document.documentElement
//window.devicePixelRatio获取设备像素比(设备像素比 = 物理像素 / 设备独立像素)
var dpr = window.devicePixelRatio || 1
// adjust body font size
// 根据 dpr 设置body 下的font-size
function setBodyFontSize () {
if (document.body) {
document.body.style.fontSize = (12 * dpr) + 'px'
}
else {
document.addEventListener('DOMContentLoaded', setBodyFontSize)
}
}
setBodyFontSize();
// set 1rem = viewWidth / 10
// 根据页面宽度设置 rem 的单位
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit()
// reset rem unit on page resize
//在窗口大小改变之后,就会触发resize事件.
//当一条会话历史记录被执行的时候将会触发页面显示(pageshow)事件
window.addEventListener('resize', setRemUnit)
window.addEventListener('pageshow', function (e) {
if (e.persisted) {
setRemUnit()
}
})
// detect 0.5px supports
// 当dpr >=2的时候设置border为0.5px
if (dpr >= 2) {
var fakeBody = document.createElement('body')
var testElement = document.createElement('div')
testElement.style.border = '.5px solid transparent'
fakeBody.appendChild(testElement)
docEl.appendChild(fakeBody)
if (testElement.offsetHeight === 1) {
docEl.classList.add('hairlines')
}
docEl.removeChild(fakeBody)
}
}(window, document))
对于大的Web应用来讲,将所有的代码都放在一个文件中显然不够有效的,特别是当你的某些代码块是在某些特殊情景下才会被使用到。webpack有一个功能就是将你的代码库分割成chunks(语块),当代码运行到需要它们的时候再进行加载。
适用场景
懒加载 JS 脚本的方式(用到这个脚本再加载它)
如何使用动态import
$ npm install @babel/plugin-syntax-dynamic-import --save-dev
// .babelrc 文件配置
{
'plugins:['@babel/plugin-syntax-dynamic-import']'
}
代码分割效果
import React from 'react';
import ReactDOM from 'react-dom';
import largeNumber from 'large-number';
import logo from './images/logo.png';
import './search.less';
class Search extends React.Component {
constructor() {
super(...arguments);
this.state = {
Text: null
};
}
// 动态import 关键代码
loadComponent() {
import('./text.js').then((Text) => {
this.setState({
Text: Text.default
});
});
}
render() {
const { Text } = this.state;
const addResult = largeNumber('999', '1');
return <div className="search-text">
{
Text ? <Text /> : null
}
{ addResult }
搜索文字的内容<img src={ logo } onClick={ this.loadComponent.bind(this) } />
</div>;
}
}
ReactDOM.render(
<Search />,
document.getElementById('root')
);
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.