webpack4.x => vue.config.js但是 !== vue.config.js


为什么要用webpack,你用的脚手架大部分都是基于这个东西,而这个东西基于nodejs,而nodejs依赖v8引擎,而v8引擎就是v8引擎。

webpack4.x从安装开始

install

1
npm install webpack -webpack-cli -D

解释:为什么webpack和webpack-cli需要分开安装,在webpack3中,webpack本身和它的CLI以前都是在同一个包中,但在第4版中已经将两者分开来更好地管理它们。webpack建议装在局部环境,虽然装在全局比较方便,但是当多个项目使用的webpack版本不一样的时候就会出现问题,即使把每个项目的webpack版本都改成一样的,又会导致原本的项目出问题,所以还是建议安装在局部环境,每个项目有独立的webpack。

mode

production:生产模式,线上的环境
development:开发模式,开发环境

sourcemap

官网介绍
mode development ‘cheap-module-eval-souce-map’ 提示比较强,打包速度比较快
mode production ‘cheap-module-souce-map’

entry

入口:每个 HTML 页面都有一个入口起点。单页应用(SPA):一个入口起点,多页应用(MPA):多个入口起点。
单个文件打包:

1
entry:"index.js"

多个文件打包成多个:

1
2
3
4
entry:{
one:'index1.js',
two:'index2.js'
}

多个文件打包成单个:

1
2
3
entry: {
main:["index1.js","index2.js"]
}

动态入口:

1
entry: () => 'index.js'

多个动态入口:

1
entry: () => new Promise((resolve) => resolve(['index1.js', 'index2.js']))

output

出口:output 位于对象最顶级键(key),包括了一组选项,指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和其他你所打包或使用 webpack 载入的任何内容」。output需要依赖node的path模块来指定当前项目的根目录。
单页面应用的输出:

1
2
3
4
output:{
path:path.resolve(__dirname,'dist'), //输出目录
filename:'bundle' //输出文件名
}

多页面输出:

1
2
3
4
output:{
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}

[name]是根据入口entry设置的变量来输出对应的文件名
配置项大概这么多:
filename,输出的文件名,可以自定义一些名称规则
path,配置输出文件存放在本地的目录
publicPath,配置CDN的路径
chunkFilename ,处理异步加载时的命名规则
hash、chunkhash和contenthash三者的区别
hash是项目级别的,每次构建得出的hash都是相同的,这可能不利于文件的缓存
chunkhash是文件级别的,值是变动修改的文件的chunkhash值
contenthash是文件级别的,在拆分css文件时记得使用处理css的缓存

module

模块:决定了如何处理项目中的不同类型的模块。防止 webpack 解析那些任何与给定正则表达式相匹配的文件。忽略的文件中不应该含有 import, require, define 的调用,或任何其他导入机制。忽略大型的 library 可以提高构建性能。这个参数还是有挺多的,可以去官网看
比如在安装完webpack之后项目目录中肯定多了一个叫node_modules的文件夹,里头有很多模块是不需要打包的,同时要识别jsx文件

1
2
3
4
5
rules: [{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}]

plugins

插件:plugins 选项用于以各种方式自定义 webpack 构建过程。webpack 附带了各种内置插件,可以通过 webpack.[plugin-name] 访问这些插件。请查看这个页面获取插件列表和对应文档,但请注意这只是其中一部分,社区中还有许多插件。比如我们做脚手架一定会用到的html-webpack-plugin,可以去看一下它的Options,就可以根据业务来做脚手架了,使用也简单,install之后引入之后

1
2
3
4
5
plugins: [
new HtmlWebpackPlugin({
...options
})
]

devServer

开发环境下的一个内置小型本地服务器: 基于nodejs实现的服务器,你也可以自己写一个,就像这样

1
2
3
4
5
6
7
8
9
10
11
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const config = require('./webpack.config.js')
const conplier = webpack(config)

const app = express()
app.use(webpackDevMiddleware(conplier,{}))
app.listen(9090,()=>{
console.log('at 9090')
})

devServer是用来提高开发效率的,它提供了一些配置项,可以用于改变devServer的默认行为,要配置devServer,除了可以在配置文件里通过devServer传入参数,还可以通过命令行传入参数。

⚠️注意!!!只有在通过devServer启动webpack时,配置文件里的devServer才会生效,因为这些参数所对应的功能都是devServer提供的,webpack本身并不认识devServer的配置项。

hot

hot配置是否启用模块的热替换功能,开启之后代码变动会自动刷新页面,做到实时热更新
两种配置方法:
1、webpack.config.js里头的devServer设置hot为true或者false,需要引入一个热更新插件

1
2
3
plugins: [
new webpack.HotModuleReplacementPlugin(),
],

2、命令行,在package.json中的script中,比如原本用来启动本地项目的命令后面加上–hot

1
"start": "NODE_ENV=development  webpack-dev-server --config  webpack.develop.config.js"

1
"start": "NODE_ENV=development  webpack-dev-server --config  webpack.develop.config.js --hot"

host

devServer服务监听的地址,如果想让局域网内的其他用户访问本项目,可以将host配置为本机的IP,通过命令行 –host 0.0.0.0也可以

port

端口:如8080,9090

open

是否打开默认浏览器,如果配置了open,在run start之后会打开默认浏览器,也可以在命令行 –open

contentBase

配置devServer,Http服务器的文件根目录

proxy配置代理,处理本地跨域,

pathRewrite

这个的应用场景是,比如项目里面写了个a.json,突然接口有问题需要换到测试接口b.json,就用这个代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
devServer: {
contentBase: './dist',
open: true,
port: 8090,
hot: true,
historyApiFallback: true,
proxy: {
'/react/api': {
target: 'http://hhardyy.com',
pathRewrite: {
'a.json': 'b.json'
}
}
}
},

devServer.historyApiFallback, 解决单页面应用无法跳转的问题,比如Route 的path=/list ,localhost:8080/list打不开

HotModuleReplacementPlugin

这个插件css修改了也不会重新渲染刷新整个页面,只是把css即时修改

1
2
3
4
const webpack= require('webpack')
plugins:[
new webpack.HotModuleReplacementPlugin()
]

两种配置bable的方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [[
'@babel/preset-env', //需要在index.js中去import "@babel/polyfill";
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
useBuiltIns: 'usage' //配置了这个的话,代码里面不用单独引入babel-polifill,它已经自动安装了
}
]]
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"absoluteRuntime": false,
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}
}

three shaking

顾名思义是摇树的意思:将一些模块中引入不用的方法摇掉,只支持import使用,因为import的底层是静态引入的方式,commonjs是动态引入的方式,开发环境(mode:development)使用three shaking要加上

1
2
3
optimization:{
usedExports: true //哪些模块被使用了再做打包
}

package.json加上

1
"sideEffects":false

假如在某个js文件内import “@babel/polly-fill”,那three
shaking发现它没有导出模块,在打包的时候会被忽略掉,但是需要使用里头的东西,为了避免打包错误,需要添加这个配置

1
"sideEffects":["@babel/polly-fill"]

false的意思是对所有的模块都进行three shaking,没有特殊处理的模块.一般如果在模块中导入了css文件,three shaking会去检测有没有模块导出,所以一般sideEffects会配置[“*.css”]

在开发环境下three shaking不会去删除没有用到的模块,因为在开发环境需要调试,如果打包上线的话three shaking其实自动就配置好了,甚至都不需要写three shaking配置。

HMR模块热替换

1
2
3
4
5
6
7
8
new webpack.HotModuleReplacementPlugin()   //启用 webpack 内置的 HMR插件

if (module.hot) {
module.hot.accept('./print.js', function() { //告诉 webpack 接受热替换的模块
console.log('Accepting the updated printMe module!');
printMe();
})
}

性能优化

code splitting

代码分割
同步代码:

1
2
3
4
5
optimization: {
splitChunks: {
chunks: 'all'
}
}

异步代码:

1
npm install --save-dev @babel/plugin-syntax-dynamic-import

.babelrc

1
plugins:["dynamic-import-webpack"]

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
function getComponent() {
return import(/*webpackChunkName:"lodash"*/'lodash').then(({
default: _
}) => {
let element = document.createElement('div')
element.innerHTML = _.join([1, 2, 3, 4, 5, 5, 5, 5, 5], '***')
return element
})
}

getComponent().then(element => {
document.body.appendChild(element)
})

webapck分析工具

git仓库

1
--profile --json > stats.json     //把打包过程的一些解释放到一个json文件里

打包分析: https://webpack.js.org/guides/code-splitting/#bundle-analysis

优化

在首屏主要代码加载完毕然后释放出网络之后再去加载模态框等其他代码用Prefetching / Preloading modules,
前端性能优化的时候,缓存其实不是最主要的点,而最主要的点应该是放到代码覆盖率上

css代码分割

可以看一下这里

1
2
3
4
5
6
7
output: {
filename: '[name].js',
chunkFilename: '[name].chunk.js', //加上这个,然后用上下面的插件
path: path.resolve(__dirname, '../dist')
}

plugins : MiniCssExtractPlugin

例如在index.js里面import 1.css和import 2.css,跑起来的时候就会将1.css和2.css合并到main.css里面,
只能用在生产环境,因为不支持HMR,用在开发环境的话会影响开发效率。

css代码压缩

用optimize-css-assets-webpack-plugin这个插件

1
npm install optimize-css-assets-webpack-plugin -D

使用方法:

1
2
3
4
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})],
},

假如要把index.js和index1.js和indexn.js中引用的css文件单独分割到styles文件里面

1
2
3
4
5
6
7
8
9
10
11
12
optimization:{
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true, //忽略掉所有默认配置
},
},
},
}

performance:false //不让提示性能上的问题

缓存

每次webpack打包的文件放到服务器,第二次请求服务器会默认请求缓存

1
2
3
4
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
}

这么配置之后如果源代码没有改变,那么打包生成的cantenthash永远不变,根据content产生的hash字符串,老版本webpack如果发现没改变代码但是打包的hash值不一样,这时候需要做额外配置

1
2
3
4
5
optimization: {
runtimeChunk: {
name: 'runtime'
},
}

在用webpack打包的时候,main.js放的是业务逻辑,vendors放的是用的库,比如jquery,然后main.js和vendors其实是有关联的,处理这些关联的内置代码是manifest,默认存在两个文件里面,它在每次打包的时候在旧版有差异,正是这些差异导致在打包的时候虽然没有改动源代码,但是两个文件里面的manifest实际上已经跟着变了。

shimming的作用

比如在index.js文件里

1
2
import $ from 'jquery'
import { ui } from 'jquery.ui.js'

其中jquery.ui.js

1
2
3
export function ui(){
$('body').css('background','red')
}

打包之后会报错说$未定义,因为每个模块只能使用模块内部的代码,那如果有一个库,又想用又不能修改内部的代码,就可以利用webpack自带的api

1
2
3
4
5
6
import webpack from 'webpack'
plugins:[
new webpack.ProviderPlugin({
$:jquery
})
]

意思就是模块内发现使用$的时候,就自动引入jquery

模块this

如果想让加载的模块this指向window而不是自己(默认指向自己)

1
npm install imports-loader --save-dev

然后再在test 的.js文件使用loader

1
2
3
4
5
6
7
8
9
10
test:/\.js$/,
exclude:/node_modules/,
use:[
{
loader:'babel-loader'
},
{
loader:'imports-loader?this=>window'
}
]

全局变量

如何在webpack打包的过程中使用全局变量
分两个环境
webpack.prod.js && webpack.dev.js && webpack.comm.js(公共配置)
1、module.exports = prodConfig
2、module.exports = devConfig

1
2
3
4
5
6
7
8
9
10
11
let merge = require('webpack-merge')
let commConfig = {
...公共webpack配置
}
module.exports = (env)=>{
if( env && env.production === 'hardy' ){
return merge(commConfig, prodConfig)
}else{
return merge(commConfig, devConfig)
}
}

env是package.json传入的全局变量,例如package.json里头的打包命令设置为

1
2
"dev":"webpack-dev-server --config ./build/webpack.comm.js"
"build" : "webpack --env.production==='hardy' --config ./build/webpack.comm.js"

如何开发一个库

1
2
3
4
5
6
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'library.js',
library:'library',
libraryTarget: 'umd'
}

libraryTarget: ‘umd’ =>
import library from ‘library.js’ ES6
const library = require(‘library’) Common
require([‘library’,function()]) AMD
library:’library’ =>支持script引入方式 意思就是打包生成的js文件挂载到页面的全局变量中然后直接library.调用里头的方法

externals

可以是字符串,数组,对象

1
externals: ['lodash']

防止用户在import 再打包的时候项目中有两个lodash,不过打包之后没有lodash,需要自己引入
,就是当lodash被commonjs的方式引入的时候,名字必须是lodash,也就是const lodash = require(‘lodash’),而不能写成const _ = require(‘lodash’)

1
2
3
4
5
externals: {
lodash: {
commonjs:'lodash'
}
},

root的作用是当以script src的方式引入的时候页面上必须注册一个名为_的全局变量

1
2
3
4
5
6
externals: {
lodash: {
root: '_',
commonjs: 'lodash'
}
},

PWA

Progressive Web Application === PWA =>遇到断网的情况下依然可以访问,用户体验更好

1
npm install workbox-webpack-plugin --save-dev

1
2
3
4
5
6
const workboxWebpackPlugin = require('"workbox-webpack-plugin"')

new workboxWebpackPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})

ts打包配置

打包ts文件的时候,需要在项目根目录下创建一个名为tsconfig.json的文件

1
npm install @types/lodash --save-dev

eslint

eslint

1
2
npm install eslint --save-dev
npx eslint --init

检测src下面的代码符不符合规范要求

1
npx eslint src

babel-eslint是较常用的eslint解析器,为了避免团队成员使用不同的编辑器导致的eslint检测区别,可以使用eslint-loader

1
npm install eslint-loader --save-dev

1
2
3
4
5
6
7
8
module:{
rules:[
{
test:/\.js$/,
use:['babel-loader',eslint-loader] //loader执行顺序从后往左
}
]
}

加完之后可以在webpack.config.js的devServer加入

1
overlay: true

这样npm run dev的时候一旦出现eslint的问题,就会在浏览器上弹窗出打包遇到的问题

webpack性能优化

提升打包速度

跟上技术的迭代 升级webpack,npm|yarn|node

尽可能少的模块使用loader

将this指向window的配置
test:/.js$/,
exclude:/node_module/,
use:[“import-loader?this=>window”]

Plugin尽可能精简并确保可靠

插件DellPlugin提高打包速率

每次打包的时候比如react,react-dom.lodash的代码都是不会变的,所以只在第一次打包的时候去分析,理想的打包状态

1
2
3
4
5
6
7
8
9
10
11
const path = require('path');

module.exports = {
entry: {
vendors: ['react', 'react-dom', 'lodash']
},
output: {
filename: '[name].dell.js',
path: path.resolve(__dirname, '../dell')
}
}

单独生成的dell下面的vendors.dell.js这么来用

1
npm install add-asset-html-webpack-plugin --save

这个插件是往html里面再去增加静态资源

1
2
3
new AddAssetWebpackPlugin({
filepath: path.resolve(__dirname, '../dell/vendors.dell.js'),
}),

分析打包文件,然后生成打包的映射

1
2
3
4
entry: {
vendors: ['lodash'],
react: ['react', 'react-dom'],
},

DllPlugin生成映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dell/[name].manifast.json'),
}),
```
然后配置AddAssetWebpackPlugin,将它加入到html中
```javascript
new AddAssetWebpackPlugin({
filepath: path.resolve(__dirname, '../dell/vendors.dell.js'),
}),
new AddAssetWebpackPlugin({
filepath: path.resolve(__dirname, '../dell/react.dell.js'),
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dell/vendors.manifast.json'),
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dell/react.manifast.json'),
}),

这么配置之后,它会去dell/vendors.manifast.json中查找第三方模块的映射关系,如果能找到就不去打包,直接从vendors.dell.js引入(从全局变量里面拿),当需要分解的文件太多的时候,为了避免每个都要复制,可以用fs来动态加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const files = fs.readdirSync(path.resolve(__dirname, '../dell'));
const plugins = [
new HTMLWebpackPlugin({
template: './src/index.html',
}),
];
files.forEach((file) => {
if (/.*\.dell.js/.test(file)) {
plugins.push(new AddAssetWebpackPlugin({
filepath: path.resolve(__dirname, '../dell', file),
}));
}
if (/.*\.manifast.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dell', file),
}));
}
});

DellPlugin的打包逻辑就是项目中引入比如jquery,lodash等不会改变的静态资源的时候,就直接根据entry设置的文件名打包到dell目录下然后生成映射,后面每次打包就可以直接去dell目录下加载相关资源,不需要重新打包

控制包大小

thread-loader,parallel-webpack,happypack多进程打包(利用node里面的多进程,使用多个cpu)

合理使用sourceMap

综合stats分析打包结果

开发环境内存编译

打包多页面,

一般都是单页面,也就是只有一个html的页面,要打包多个页面的时候,在entry里头写两个入口

1
2
3
4
entry:{
main:'./src/index.js',
list:'./src/list.js'
}

然后可以去github上查找html-webpack-plugin的配置项,或者往上翻

1
2
3
4
5
6
7
8
9
10
11
new HTMLWebpackPlugin({
template: './src/index.html',
filename: 'index.html',
chunks: ['runtime', 'vendors', 'main'],
}),
new HTMLWebpackPlugin({
template: './src/index.html',
filename: 'list.html',
chunks: ['runtime', 'vendors', 'list'],
}),
...

如果觉得每次这样复制不太友好,可以用封装一个函数来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const makePlagins = (configs) => {
const plugins = [
// new CleanWebpackPlugin(['dist'], {
// root: path.resolve(__dirname, '../'),
// }),
];

const keys = Object.keys(configs.entry);
keys.forEach((item) => {
plugins.push(
new HTMLWebpackPlugin({
template: './src/index.html',
filename: `${item}.html`,
chunks: ['runtime', 'vendors', item],
}),
);
});

const files = fs.readdirSync(path.resolve(__dirname, '../dell'));
files.forEach((file) => {
if (/.*\.dell.js/.test(file)) {
plugins.push(new AddAssetWebpackPlugin({
filepath: path.resolve(__dirname, '../dell', file),
}));
}
if (/.*\.manifast.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dell', file),
}));
}
});

return plugins;
};

在这个操作之前先将

1
2
3
4
5
6
7
8
9
10
11
module.exports={
entry:{
....
},
output:{
...
},
plugins:{
...
}
}

改成

1
2
3
4
5
6
7
8
const configs={
entry:{
....
},
output:{
...
}
}

configs.plugins = makePlagins(configs)
这样就可以实现动态改变打包的入口文件就可以生成新的一个页面

如何编写一个loader

webpack API
写一个loader来做异常捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const loaderUtils = require('loader-utils')
module.exports = function (source) {
//异常捕获start
try(function(){

}).catch(e){}
//异常捕获end

const options = loaderUtils.getOptions(this)
const callback = this.async()

setTimeout(() => {
const result = source.replace('hardy', options.name)
callback(null, result)
}, 1000);
}

也可以用来写国际化
比如业务代码index.js中
console.log(‘webpack4.x‘)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
loader里头这么写
const loaderUtils = require('loader-utils')
module.exports = function (source) {
if (node全局变量 === '中文') {
source.replace('{{title}}', '中文标题')
} else {
source.replace('{{title}}','english title')
}

const options = loaderUtils.getOptions(this)
const callback = this.async()

setTimeout(() => {
const result = source.replace('hardy', options.name)
callback(null, result)
}, 1000);
}

编写一个plugin

1
2
3
4
"scripts": {
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js", //使用node的调试工具调试插件,--inspect开启调试工具,--inspect-brk在第一行打断点
"build": "webpack"
},

最近拿了新offer,今天也是国庆,祖国70周年的生日!!!

分享一张觉得拍的不错的美图。

红线
红线