1、webpack进阶
整个目录结构为
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
// mode: 'production',
// devtool: 'cheap-module-source-map',
entry: {
main: './src/index.js'
},
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}, {
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist']),
new webpack.HotModuleReplacementPlugin()
],
optimization: {
usedExports: true
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
1-1 Tree Shaking
当我们写了一个math.js,导出add和minus两个方法,但是index.js只导入add一个方法,我们可以通过摇树,让打包的文件中自动删除没有导入得minus的代码,但是两种模式development
和production
,稍微有些区别,
在development
模式下需要额外配置optimization: {usedExports: true}
,并且在package.json
配置"sideEffects": false
表明所有的都采用Tree Shaking
由于import '@/babel/polyfill'
import './style.css'
这种本身就没有导入,Tree Shaking会完全忽略它们,也可以配置"sideEffects": ['@/babel/polyfill','*.css']
表明遇到他们不采用Tree Shaking
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
// mode: 'production',
// devtool: 'cheap-module-source-map',
...
module: {
...
},
plugins: [
...
],
optimization: {
usedExports: true
},
...
}
// math.js
export const add = (a, b) => {
console.log( a + b );
}
export const minus = (a, b) => {
console.log( a - b );
}
// index.js
// Tree Shaking 只支持 ES Module
// ES Module底层是静态导入
// CommonJs 底层是动态导入
import { add } from './math.js';
add(1, 2);
通过 npm run bundle 得到的打包文件中会有
export provided: add minus
export used: add
development
模式下只会提示,但是仍然会打包所有代码,这是因为如果自动删除了没导入的代码,可能会导致源文件和打包后文件sourceMap对应不上,不方便调试
production
模式只需要配置sideEffects,就可以真正做到Tree Shaking
1-2 development and production
通常情况下开发环境和生成环境webpack配置是不一样的,比如在Tree Shaking时,开发环境才需要配置optimization: {usedExports: true}
,devserver
也是如此,devtool
两者也不一样
那么我们每次打包需要手动更改webpack.config,js文件,我想死,谁也别拦我😭 因此,我们可以配置两个config.js文件
// package.json
{
"scripts": {
"dev": "webpack-dev-server --config ./webpack.dev.js",
"build": "webpack --config ./webpack.prod.js"
},
}
还可以优化,将webpack.dev.js 和 webpack.prod.js 公共的配置抽离到webpack.common.js,并新建一个build文件夹,将三个文件放在build下(有没有很像vue2的目录结构😋)
webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}, {
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'],{
{
root: path.resolve(__dirname, '../') //❗ 地址改成build同级
}
})
],
output: {
filename: '[name].js',
// path: path.resolve(__dirname, '../', 'dist') //❗ 生成打包文件地址改成build同级
path: path.resolve(__dirname, '../dist') //❗ 生成打包文件地址改成build同级
}
}
webpack.dev.js
const webpack = require('webpack');
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
optimization: {
usedExports: true
}
}
module.exports = merge(commonConfig, devConfig);
webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map'
}
module.exports = merge(commonConfig, prodConfig);
// package.json
{
"scripts": {
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
},
}
npm run dev
用于开发环境
npm run build
用于生产环境
1-3 Code Splitting
webpack 默认会将所有的模块打包到一个文件中,如 main.js 。
有时,某些第三方模块变动较少,如:jQuery、lodash 。我们可以将这些模块打包到单独的文件中。
- 1、通过手动分割
// lodash.js
import _ from 'lodash'
window._ = _
// index.js
console.log(_.join(['a','b','c'],'---'))
然后以之前提到的配置多入口的方式进行打包,通过HtmlWebpackPlugin将两个文件引入到html中
module.exports = {
entry: {
lodash: './src/lodash.js',
main: './src/index.js'
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'],{
{
root: path.resolve(__dirname, '../') //❗ 地址改成build同级
}
})
],
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../dist') //❗ 生成打包文件地址改成build同级
}
}
- 2、
import _ from 'lodash'
同步模块
只需要配置optimization
即可
// index.js
import _ from 'lodash'
console.log(_.join(['a','b','c'],'---'))
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
main: './src/index.js'
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
],
optimization: {
splitChunks: {
chunks: 'all'
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../dist')
}
}
打包出来main.js 和 vendors~main.js,并引入到html中
- 3、异步模块
// index.js
function getComponent() {
return import('lodash').then(({ default: _ }) => {
var element = document.createElement('div');
element.innerHTML = _.join(['a', 'b', 'c'], '---');
return element;
})
}
getComponent().then(element => {
document.body.appendChild(element);
});
无需做任何配置,会自动进行代码分割,放置到新的文件中main.js 和 0.js
/* webpackChunkName: "jquery" */
称为魔法注释 magic comment,它可以指定打包生成的模块的文件名。需要使用@babel/plugin-syntax-dynamic-import
才可以使用magic comment
// .babelrc
{
presets: [
[
"@babel/preset-env", {
targets: {
chrome: "67",
},
useBuiltIns: 'usage'
}
],
"@babel/preset-react"
],
plugins: ["@babel/plugin-syntax-dynamic-import"]
}
1-4 Lazy Loading
懒加载并不是webpack当中的概念,而是ES中的概念
这种就是一次性加载所有的代码
// index.js
import _ from 'lodash'
document.addEventListener('click', () => {
var element = document.createElement('div')
element.innerHTML = _.join(['hello', 'world'], '-')
document.body.appendChild(element)
})
下面这种异步代码的写法可以实现一种懒加载的行为,在点击界面的时候才会去加载需要的模块,效果就是我们在页面中,开始只会加载一个main.js,然后点击一下页面会在加载一个loadsh函数,调用这个函数的某些方法我们实现了一个字符串的拼接过程,最终呈现在了页面上。
function getComponent() {
return import(/* webpackChunkName: "lodash" */ 'lodash').then(
({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['hello', 'world'], '-')
return element
}
)
}
document.addEventListener('click', () => {
getComponent().then(element => {
document.body.appendChild(element)
})
})
使用 ES7 的 async 和 await 后,上面代码可以改写成下面这种写法,效果等同
async function getComponent() {
const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash')
const element = document.createElement('div')
element.innerHTML = _.join(['hello', 'world'], '-')
return element
}
document.addEventListener('click', () => {
getComponent().then(element => {
document.body.appendChild(element)
})
})
1-5 打包分析
首先修改下package.json
// package.json
"scripts": {
"dev": "webpack --profile --json > stats.json --config webpack.dev.js"
}
npm run dev
生成stats.json,对打包过程的描述文件
1-6 preloading prefetching
写在前面,chrome浏览器有一个查看code coverage的功能,也就是F12后ctrl+shift+p,输入coverage查看代码的使用率
先来一段最原始的代码
// index.js
document.addEventListener('click', () => {
var element = document.createElement('div')
element.innerHTML = 'nice day!'
document.body.appendChild(element)
})
打包之后click的function中的代码是没有使用的,修改一下
// index.js
document.addEventListener('click', () =>{
import('./click.js').then(({default: func}) => {
func();
})
});
// click.js
function handleClick() {
const element = document.createElement('div');
element.innerHTML = 'Dell Lee';
document.body.appendChild(element);
}
export default handleClick;
这样修改之后代码的利用率就高一些,也节约首屏加载的时间
所以 webpack 做代码分割打包配置时 chunks 的默认是 async,而不是 all 或者 initial;因为 webpack 认为只有异步加载这样的组件才能真正的提高网页的打包性能,而同步的代码只能增加一个缓存,但是第一次还是需要加载很多资源,实际上对性能的提升是非常有限的
鉴于此,我们还是希望能够通过异步加载的方式,来加载我们的模块代码,但是又怕加载很慢。比如,我们加载网站首页的时候,可以通过异步加载登录模态框模块,但是又怕点击登录,模态框模块很慢,于是就有了prefetching
网络空闲去加载异步代码 preloading
和主业务文件一起加载的
/* webpackPrefetch: true */
/* webpackPreload: true */
import(/* webpackPrefetch: true */'./click.js')
这样就可以在网站首页加载完成,带宽释放出来之后,默默为我们加载登录模态框模块的代码,既满足了首页加载快的需求,又满足了登录加载快的需求
1-7 CSS文件的代码分割
之前提到过style-loader
是将css-loader
解析的css插入到hede的<style>
中,现在我需要也在dist下生成css文件
旧版本的 MiniCssExtractPlugin 因为不支持HMR,所以最好只在生产环境中使用,如果放在开发环境中,更改样式后需要手动刷新页面,会降低开发的效率;新版本已支持开发环境中使用HMR
// webpack.common.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css'
})
]
}
// index.js
import './style.css'
console.log("css")
// style.css
body{background:red}
其中如果是被html直接引用的,会走filename
,间接就是chunkFilename
,打包之后dist/main.css main.css.map
// index.js
import './style.css'
import './style1.css'
console.log("css")
// style.css
body{background:red}
// style.css
body{font-size:20px}
打包之后main.css
body{background:red}
body{font-size:20px}
/*# sourceMappingURL=main.css.map*/
- OptimizeCSSAssetsPlugin
// webpack.common.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({})]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css'
})
]
}
打包之后main.css
body{background:red;font-size:20px}
- 一个入口文件引入多个 css 文件,默认将其打包合并到一个 css 里
- 多个入口文件引入不同的 css 文件,打包默认会产生多个 css 文件,可通过配置,使其合并为一个 css 文件
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles', // 打包后的文件名
test: /\.css$/, // 匹配所有 .css 结尾的文件,将其放到该组进行打包
chunks: 'all', // 不管是同步还是异步加载的,都打包到该组
enforce: true // 忽略默认的一些参数(比如minSize/maxSize/...)
}
}
}
}
- 多个入口文件引入多个 css 文件的打包
走你extracting-css-based-on-entry
1-8 webpack与浏览器缓存
之前打包的文件名字都一样,浏览器已经有了缓存,现在工程化已经可以帮我们实现类似数据签名的东西,文件内容改变hash改变,这样浏览器就会读取新内容 abc.agagjlagjlkhjlj23.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
}
这样引入的第三方库的hash不会变,浏览器就可以使用缓存,提高网站加载效率
但是在老版本中,因为mainfset
的原因,在main.js和vendor第三方库之间的关联代码叫manifest,打包mainfset不同,可能导致打包出来的文件内容变化,hash就不同。
可以多加一些配置
optimization: {
runtimeChunk: {
name: 'runtime' //将mainfest打包到单独的js文件中 runtime.ajalgjalg.js
},
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
}
}
},
performance: false //不提示性能的信息
1-9 Shimming
webpack是基于模块化打包的,我们知道,在一个模块中的数据,在其他模块是访问不到了
// index.js
import $ from 'jquery'
import _ from 'lodash'
import { ui } from './jq_dh.ui'
ui()
$(body).html("abcd")
// jq_dh.ui.js
export function ui(){
$('body').css('background',_.join(['r','e','d'],''))
}
在jq_dh.ui.js中 $
和-
找不到的,会报错,那么我们可以通过webpack提供的一个插件ProvidePlugin
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
entry: {
main: './src/index.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader'
}, {
loader: 'imports-loader?this=>window' //模块this指向window
}]
}, {
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
}),
new webpack.ProvidePlugin({
$: 'jquery', //模块中有使用$帮引入jquery
_: 'lodash', //模块中有使用_帮引入lodash
_join: ['lodash', 'join']
}),
],
optimization: {
runtimeChunk: {
name: 'runtime'
},
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
}
}
}
},
performance: false,
output: {
path: path.resolve(__dirname, '../dist')
}
}
这样就可以实现Shimming垫片的功能 还有一个需求,模块中this都默认指向模块本身
// index.js
console.log(this === window) // false
可以通过imports-loader
来实现将this指向window
1-10 环境变量
之前提到通过不同的配置文件来打包,将公共的抽离出来放在webpack.common.js当中
"scripts": {
"dev-build": "webpack --config ./build/webpack.dev.js",
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js"
},
也可以通过环境变量的使用,通过环境变量去判断导出merge(commonConfig,devConfig)
还是merge(commonConfig,prodConfig)
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const merge = require('webpack-merge');
const devConfig = require('./webpack.dev.js');
const prodConfig = require('./webpack.prod.js');
const commonConfig = {
entry: {
main: './src/index.js'
},
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
}, {
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'sass-loader',
'postcss-loader'
]
}, {
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'],{
{
root: path.resolve(__dirname, '../') //❗ 地址改成build同级
}
})
],
output: {
filename: '[name].js',
// path: path.resolve(__dirname, '../', 'dist') //❗ 生成打包文件地址改成build同级
path: path.resolve(__dirname, '../dist') //❗ 生成打包文件地址改成build同级
}
}
module.exports = (env) => {
if(env && env.production) {
return merge(commonConfig, prodConfig);
}else {
return merge(commonConfig, devConfig);
}
}
// webpack.dev.js
const webpack = require('webpack');
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
optimization: {
usedExports: true
}
}
module.exports = devConfig
// webpack.prod.js
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map'
}
module.exports = prodConfig;
在package.json中传入不同的环境变量
"scripts": {
"dev-build": "webpack --config ./build/webpack.common.js",
"dev": "webpack-dev-server --config ./build/webpack.common.js",
"build": "webpack --env.production --config ./build/webpack.common.js"
},
2、webpack实战
2-1 打包库文件
需求:想写一个自己的库文件
// index.js
import * as math from './math';
import * as string from './string';
export default { math, string }
// math.js
export function add(a, b) {
return a + b;
}
export function minus(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function division(a, b) {
return a / b;
}
// string.js
export function join(a, b) {
return a + ' ' + b ;
}
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'libralry.js',
library: 'root',
libraryTarget: 'umd'
}
}
library: 'root'
可以用过<script>
引入使用,并在全局挂载一个root变量
libraryTarget: 'umd'
使用umd规范,可以通过ES Module CommonJs AMD CMD引入使用,还有其他值this->this.library global->global.library window->window.library
另外的需求:string.js中引入了lodash,但是在使用libralry.js时已经引入过lodash,这样就会打包两次lodash,可以通过配置externals
// string.js
import _ from 'lodash';
export function join(a, b) {
return _.join([a, b], ' ');
}
import _ from 'lodash'
import libralry from 'libralry'
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.js',
externals: ['lodash'],
// externals: {
// lodash: {
// root: '_', // 通过全局script标签引入,并在页面中注入_为全局变量的lodash
// commonjs: 'lodash' //通过模块引入 导入名也为lodash
// }
// }, // 第二种配置方式
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'library.js',
library: 'root',
libraryTarget: 'umd'
}
}
2-2 PWA
Progressive Web Application 渐进式web应用程序,但我们第一次访问我们的应用时,后续不管服务器是不是宕机,都可以继续访问到
// index.js
console.log('hello, this is dell');
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('service-worker registed');
}).catch(error => {
console.log('service-worker register error');
})
})
}
我们可以只用在webpack.prod.js中配置pwa,关注线上的体验,开发时不用管
const WorkboxPlugin = require('workbox-webpack-plugin');
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css'
}),
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]
使用 npm install http-server --save-dev
开启一个简单的服务器
script:{
"start": "http-server dist"
}
2-3 TypeScript
TypeScript是javaScript的超集,增加了类型检测系统,可以说把js从一个动态弱类型,转变成一个静态强类型吧
需要安装ts-loader
typescript
const path = require('path');
module.exports = {
mode: 'production',
entry: './src/index.tsx',
module: {
rules: [{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
还要在根目录下创建tsconfig.json
{
"compilerOpitons": {
"outDir": "./dist",
"module": "es6",
"target": "es5",
"allowJs": true,
}
}
但是需要注意一点,我们在业务代码中引入了jquery、lodash这样的模块,ts可能不会给我们检测,我们需要格外的去安装相应的类型文件@types/jquery``@types/lodash
,可以从 TypeSearch 中找到并安装这些第三方库的类型声明文件
2-4 devServer请求转发
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true,
proxy: {
'/api': {
target: 'localhost:3000',
secure: false, //https
pathRewrite: {'^/api' : ''}
changeOrigin: true, //有些网站对origin做了限制,防止爬虫
headers: {
host: 'localhost:8080',
cookie: 'cookie'
}
}
}
}
2-5 webpack中ESlint
使用ESlint方式
- 使用
npx eslint src
命令行查看 - 使用 vscode中安装eslint插件
// .eslintrc.js
module.exports = {
"extends": "airbnb",
"parser": "babel-eslint",
"rules": {
"react/prefer-stateless-function": 0, //无状态组件写成函数式
"react/jsx-filename-extension": 0 // jsx
},
globals: {
document: false //document不允许被覆盖
}
}
- 使用
eslint-loader
结合overlay:true
module.exports = {
entry: {
main: 'src/index.js'
},
devServer: {
overlay: true
contentBase: './dist',
open: true,
port: 8080,
hot: true
}
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
// loader: 'babel-loader',
// use: ['babel-loader', 'eslint-loader'],
use: [{
loader: 'eslint-loader',
options: {
fix: true
},
force: 'pre'
}, 'babel-loader']
}]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
第三种方式,不用依赖编辑器,也不用看命令行,是很好的一种方式,但是增加loader势必会降低打包的速度
因此有的会使用 git上传中的钩子去执行eslint,有问题不让上传,自己去改好才能上传,这就需要大家去权衡了
2-6 webpack性能优化
- 跟上技术的迭代(node npm yarn webpack)
- 尽可能少使用loader,合理使用exclude 和 include
- plugins尽可能精简并且可靠
- resolve参数合理配置
// webpack.config.js
module.exports = {
resolve:{
extensions: ['.js', '.jsx'],
mainFiles: ['index', 'child'],
alias: {
@: path.resolve(__dirname, './src/login')
}
}
}
- DllPlugin提高打包速度
实现第三方包只打包一次,在我们项目中引入一些库,我们不想每次都打包他们,只需要打包一次 首先我们再build下新建webpack.dll.js,把第三方库单独进行打包到vendors.dll.js中,
// webpack.dll.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash'],
react: ['react', 'react-dom'],
jquery: ['jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]' // //打包生成的库名,通过全局变量的形式暴露到全局
},
plugins: [
new webpack.DllPlugin({ //对暴露到全局的代码进行分析,生成vendors.manifest.json 的映射文件
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json'),
})
]
}
// webpack.common.js
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');
const plugins = [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
];
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
if(/.*\.dll.js/.test(file)) {
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
}))
}
if(/.*\.manifest.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifmest: path.resolve(__dirname, '../dll', file)
}))
}
})
module.exports = {
entry: {
main: './src/index.js',
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [{
test: /\.jsx?$/,
include: path.resolve(__dirname, '../src'),
use: [{
loader: 'babel-loader'
}]
}, {
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}]
},
plugins,
optimization: {
runtimeChunk: {
name: 'runtime'
},
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
}
}
}
},
performance: false,
output: {
path: path.resolve(__dirname, '../dist')
}
}
webpack.common.js中通过AddAssetHtmlWebpackPlugin
插件将生成的xx.dll.js文件引入到html中,通过DllReferencePlugin
插件将js文件的映射文件导入,这样就可以手动打包第三方库,项目中直接使用
在package.json中
"scripts": {
"dev-build": "webpack --config ./build/webpack.dev.js",
"dev": "webpack-dev-server --config ./build/webpack.dev.js",
"build": "webpack --config ./build/webpack.prod.js",
"build:dll": "webpack --config ./build/webpack.dll.js"
},
先通过npm run build:dll
生成js文件和相应的mainfest文件,然后npm run dev-build
就可以不用打包第三方库直接使用
- 控制包文件大小
项目中不使用的第三方模块要通过Tree Shaking去不打包,也可以就不要引入,也可以通过splitChunkPlugin
对代码进行拆分
- thread-loader paraller-webpack happypack
webpack是基于node的,所以是单进程的,我们可以使用thread-loader多进程打包,或者paraller-webpack进行多页面打包,happypack 等来进行多进程打包,从而提高打包速度
HappyPack就能让Webpack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程
module: {
rules: [{
test: /\.js$/, //把对.js文件的处理转交给id为babel的HappyPack实例
use: 'happypack/loader?id=babel',
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/
}, {
test: /\.css$/, //把对.css文件的处理转交给id为css的HappyPack实例
use: 'happypack/loader?id=css',
include: path.resolve(__dirname, 'src')
}],
noParse: [/react\.min\.js/]
}
plugins: [
//用唯一的标识符id来代表当前的HappyPack是用来处理一类特定文件
new HappyPack({
id: 'babel',
//如何处理.js文件,和rules里的配置相同
loaders: [{
loader: 'babel-loader',
query: {
presets: [
"env", "react"
]
}
}]
}),
new HappyPack({
id: 'css',
loaders: ['style-loader', 'css-loader'],
threads: 4, //代表开启几个子进程去处理这一类型的文件
verbose: true //是否允许输出日子
})
]
- 合理使用SourceMap
- 结合stats分析打包结果
- 开发环境内存编译
- 开发环境无用插件剔除
2-7 多页面配置
多页面配置实际上是配置多个entry,在之前Code Splitting
中也提到过这样生成的两个js会引入到html中,现在我们要通过多配置几个HtmlWebpackPlugin
插件,将js文件分别引入就可以了
// webpack.common.js
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');
const makePlugins = (configs) => {
const plugins = [
new CleanWebpackPlugin(['dist'], {
root: path.resolve(__dirname, '../')
})
];
Object.keys(configs.entry).forEach(item => {
plugins.push(
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: `${item}.html`,
chunks: ['runtime', 'vendors', item]
})
)
});
const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
files.forEach(file => {
if(/.*\.dll.js/.test(file)) {
plugins.push(new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
}))
}
if(/.*\.manifest.json/.test(file)) {
plugins.push(new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
}))
}
});
return plugins;
}
const configs = {
entry: {
index: './src/index.js',
list: './src/list.js',
detail: './src/detail.js',
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [{
test: /\.jsx?$/,
include: path.resolve(__dirname, '../src'),
use: [{
loader: 'babel-loader'
}]
}, {
test: /\.(jpg|png|gif)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240
}
}
}, {
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader'
}
}]
},
optimization: {
runtimeChunk: {
name: 'runtime'
},
usedExports: true,
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
name: 'vendors',
}
}
}
},
performance: false,
output: {
path: path.resolve(__dirname, '../dist')
}
}
configs.plugins = makePlugins(configs);
module.exports = configs
3 webpack深入
3-1 loader的编写
新建一个空项目,src下写业务,在根目录下新建loaders文件夹,写一个replaceLoader.js
// index.js
console.log("hello world")
// replaceLoader.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
// const options = this.query
const options = loaderUtils.getOptions(this)
// return source.replace('world', options.name);
const result = source.replace("world", options.name)
this.callback(null,result)
}
这里需要注意的是函数不要写箭头函数,因为this。options的参数可以通过this.query来取,但是官方推荐使用loader-utils
。同步loader可以直接return,仅仅只能返回内容,也可以通过this.callback()
来返回更多内容
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
// module: {
// rules: [{
// test: /\.js$/,
// use: [
// {
// loader: 'path.resolve(__dirname, './loaders/replaceLoader.js')', //第一种方式
// options: {
// name: "世界"
// }
// }
// ]
// }]
// },
resolveLoader: {
modules: ['node_modules', './loaders'] //先在node_modules中找loader,没找到,再在loaders里找
},
module: {
rules: [{
test: /\.js$/,
use: [
{
loader: 'replaceLoader', //第二种方式引入loader
options: {
name: "世界"
}
}
]
}]
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
}
}
这样使用了自己编写的loader就可以将业务代码中的world改成世界
以上的还有一种异步loader,通过this.async
来提示这是一个异步loader,但是需要注意:如果你使用了use:['loader','asyncLoader']
那么需要先等asyncLoader解析完
// replaceLoaderAsync.js
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
const callback = this.async();
setTimeout(() => {
const result = source.replace('world', options.name);
callback(null, result);
}, 1000);
}
自定义loader场景
- 异常捕获,将我们业务代码的function(){},通过loader打包成try{function(){}}catch(e){}
- 中英文切换,假设我们有一个{{title}}
if(node全局变量 === '中文'){
source.replace('{{title}}', '中文标题')
}else{
source.replace('{{title}}', 'english title')
}
3-2 plugin的编写
新建一个空项目,src下写业务,在根目录下新建plugins文件夹,写一个copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
console.log('compiler');
})
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
// debugger;
compilation.assets['copyright.txt']= {
source: function() {
return 'copyright by dell lee'
},
size: function() {
return 21;
}
};
cb();
})
}
}
module.exports = CopyrightWebpackPlugin;
const path = require('path');
const CopyRightWebpackPlugin = require('./plugins/copyright-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [
new CopyRightWebpackPlugin()
],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
loader
和plugin
的区别
当需要引用js文件或者其他格式的文件时,可以使用loader去帮助处理这个文件,帮助我们去处理模块
plugin在我们打包的时候,就会生效,比如打包生成个html文件,就使用htmlWebpackplugin,当打包之前需要清理文件夹,就使用cleanWebpackPlugin
loader本质上是一个函数,plugin本质上是一个类(所以使用时才需要new)
// 调试
"scripts": {
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
"build": "webpack"
},