随着前端技术体系逐渐成熟,前端开发人员的必备技能已经不仅仅局限于简单的页面的编写,而是有越来越多工程化的事情需要我们去考虑。怎么才能让一个项目的开发变的顺畅,不需要考虑太多的浏览器兼容的问题?如何降低代码的冗余,减少生产环境代码的体积?如何提升用户使用产品时的体验?等等这些问题都是需要我们去考虑的。本文就以上问题,梳理出一个常规的webpack配置文件,简化你的学习成本,提高生产效率。
我们先从代码的兼容说起~这里我们不得不提到babel
什么是Babel?
Babel是一套主要用来将使用ECMAScript2015+语法编写的代码转换成纯ES5的Javascript代码的工具,以兼容任何老式浏览器与运行环境。
Babel可以做什么?
Babel可以用来编译ES6+的语法,它使所有ES6+规范新增的语法糖都可用,包括:类(class),箭头函数(arrow function),模板字符串等等。
我们在webpack中如何使用babel呢?
首先创建一个.babelrc文件,配置如下
{
"presets": [
"@babel/preset-react",
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
},
"corejs": "3",
"useBuiltIns": "usage"
}
]
]
}
preset-env是一个预设的集合,因为我们很难知道,我们编写的业务代码到底需要引入哪些转换的插件,preset-env会根据我们的配置需要,去帮我们做插件的引用
useBuiltIns:usage
这个配置很关键,有很多ES5+的新语法,浏览器普遍还没有进行支持,所以需要借助一些polyfill来帮助我们去实现这些新语法的特性,如果我们不设置useBuiltins这个属性的话,babel会根据targets的配置,加载一些指定浏览器所不具备的功能函数,但是其中有很多函数其实我们并没有用到,usage这个属性只会加载我们代码中用到的新语法,这样可以让polyfill按需加载,大幅度的降低打包后文件的体积。
然后在webpack.config.js文件中添加
module: {
rules: [{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/, // 不匹配选项
},
]
},通过上面的babel配置,我们就能愉快的使用各种版本的新语法,而不需要担心浏览器兼容的问题。
实现了代码转换后,我们要怎么注入到html文件中去呢?总不能每次对js文件进行打包后,手动通过script标签的方式,引入到html文件中吧,这样一个文件还能配置的过来,分包后多个文件怎么办?
htmlWebpackPlugin
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/tepmlate/index.html'),
title: 'name',
file: 'index.html',
inject: true
}),通过这个插件,我们打包后的js文件就能够被注入到我们的模板文件中去。
js、html我们都说了,接下来说说关键的css配置
css配置
我们在开发的过程中,通常会会遇到一些css的问题,比如:css怎么模块化?怎么使用less等css预处理语言?怎么让css命名不污染全局?我们通过以下webpack配置,来解决这些问题
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: true,
}
},
'postcss-loader',
'less-loader'
]
},
style-loader能够帮我们把css注入到style标签里。
css-loader能够帮助我们在js文件中实现css文件的导入
less-loader用来处理less语言
postcss-loader用来支持postcss对css的处理
css-loader配置中的modules属性能够实现css的范围化。这样能够保证局部模块开发不会影响全局。
注意css-loader options中的importLoaders,在我们less文件中也有可能通过@import的方式引入其他的css文件,引入的这些文件也需要做一些处理,importLoaders:2指定,引入的css文件先经过前面两个loader转换。loader的执行顺序是从下到上,从右向左
postcss.config.js
module.exports = {
modules: true
plugins: [
require('autoprefixer')
]
}autoprefixer这个插件,能够帮我们去对一些新的css属性进行浏览器的兼容,会在这些属性前面自动添加前缀。
通过style-loader我们可以将css注入到style标签中,但是这样会加大html文件的大小,后期也无法实现css的缓存和压缩,我们可以使用插件,将css文件拆分出去。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
new MiniCssExtractPlugin({
filename: "style.[contenthash].css",
}),
静态资源引入
我们在开发的使用,一般会通过相对路径去引入一些图片等静态资源,但是当我们打包后,文件的路径产生了变化,这时候之前的相对路径有可能会失效,我们怎么解决这个问题呢?
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}]
}
使用url-loader可以让我们引入的静态资源在打包后的html文件中有一个正确的引入路径。
这里只是对图片做了一个展示,其他的音视频、文件等文件,也可以通过这种方式。
limit限制文件的大小单位是bit,低于limit限制,图片会被转换成base64格式,这样做的优势是可以减少一次http请求,但是base64会加大css文件的体积,所以limit尽量控制在一个合理范围。
说完了上面的基础配置,我们已经能够流畅的实现业务代码的开发,不用再去担心兼容等问题。
下面我会简单的介绍一些优化方面的内容,使编译更加迅速,让页面加载更加流畅
提升打包速度
webpack4为我们带来了mode,通常会用到的有development和production两种模式,这两种模式为我们简化了很多配置,一些之前需要我们去手动配置的属性,这两种模式会帮我们实现
具体做了哪些简化,大家可以去官网查阅,传送地址webpack.docschina.org/concepts/mo…
module.exports = {
mode: 'production'
};模式设置为production后,最直观的体现就是,代码的体积大幅度降低。
(development模式)
(production模式)
我们可以看到,同样的文件,在development模式下,打包后的体积是131kb而在production模式下,打包后的体积是24.9kb,这还只是一个小小的项目,我们的项目越大,这种差距也就更加的明显。
production模式会开启tree shaking,实现代码的按需打包,使用 ES2015 模块语法(即 import 和 export)引入的包,我们不再一股脑的全部打包出去,而是只打包我们页面中引用的部分,这样也会降低代码的体积。
第三方模块打包
我们会使用要一些第三方的模块,这些模块在很多时候都不会频繁的变动,所以就没有必要再每一次打包的时候都去对这些第三方模块重新打包一次。我们可以使用DllPlugin插件来做一些处理,让第三方包和业务包拆分开来。
const webpack = require('webpack')
const path = require('path')
module.exports = {
mode: 'production',
entry: ["react", "react-router", "react-redux", "redux", "react-dom"],
output: {
path: path.resolve(__dirname, "src"),
filename: "vender.[hash].js",
library: "vender"
},
plugins: [
new webpack.DllPlugin({
name: "[name]",
context: __dirname,
path: path.resolve(__dirname, "src/manifest.json")
})
]
}先将第三方模块打包一次,会生成一个打包后的文件和manifest.json文件。这个json文件包含了第三方包的映射关系
new webpack.DllReferencePlugin({
context: __dirname,
manifest: path.resolve(__dirname, "src/manifest.json")
}),
new AddAssetHtmlPlugin([{
filepath: path.resolve(__dirname, 'src/dll.main.js'),
}]),
然后在写业务代码时,配置中添加如上代码,这样在打包的时候,就能够避开第三方包,不需要重复打包。然后使用AddAssetHtmlPlugin这个插件将先前打包的第三方包注入到html文件中
模块拆分
webpack4默认配置
splitChunks: {
chunks: "async",
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
chunks: 可填 async, initial, all. 顾名思义,async针对异步加载的chunk做切割,initial针对初始chunk,all针对所有chunk。
minSize: 我们切割完要生成的新chunk要>30kb。
minChunks: 共享该module的最小chunk数
maxAsyncRequests:最多有5个异步加载请求该module
maxInitialRequests: 初始化的时候最多有3个请求该module
cacheGroups: 这个就是重点了,我们要切割成的每一个新chunk就是一个cache group。
通过这些配置可以实现第三方包、公共模块、业务代码的拆分,具体拆分方式根据实际需求来定。一般情况下,我们使用默认的配置就好,因为配合使用上面DllPlugin的拆分第三方包后,我们代码中耦合的大规模代码就很少了,所以即便重复打包,也不会对体积有太大的影响,这些公共模块在初始的时候被拆分出去的意义不是很大,而默认的异步加载就很有用处了,各位看官请看下面的详解。
模块懒加载
现在很多网站app都是采用spa这种方式进行开发。所以这会导致一些主模块会比较大,用户在首次加载的时候,会非常的缓慢。
这个时候我们可以实现模块的懒加载,首屏加载的模块和非首屏加载的模块拆开。
非首屏加载的模块在我们真的需要展示的时候才去加载。
这里就需要用到动态模块导入的知识
我们以react为例子
async componentDidMount(){
await import(/* webpackChunkName: true */ './Components/Header').then((module) => {
this.setState({
Header: module.default
})
})
}
这样只有我们去执行了componentDidMount这个生命周期后,才会去加载对应的模块。一般来说在单页面应用中,都是以页面为单位来进行代码的拆分,在这里我们依赖一个第三方包,react-loadable,实现懒加载的原理都是一样的,只不过这个包帮我们封装了一些常用的逻辑,大家有兴趣可自己去研究github.com/jamiebuilds…
import React from "react";
import Loadable from 'react-loadable';
const loading = () => {
return (
<div> loading... </div>
)
}
const Main = Loadable({
loader: () => import(/* webpackChunkName: "index" */ './Main'),
loading: loading
});
const Store = Loadable({
loader: () => import(/* webpackChunkName: "store" */ './Store'),
loading:loading
});
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
export default function App() {
return(
<React.Fragment>
< Router >
<div>
<Link to="/">首页</Link>
<Link to="/store">商城</Link>
</div>
<Switch>
<Route path='/' exact>
<Main></Main>
</Route>
<Route path="/store">
<Store> </Store>
</Route>
</Switch>
</Router>
</React.Fragment>
这里我写了两个页面,一个Main首页和Store商城页面,当我们路由切换到对应的界面后,才会去加载
多页面应用如何配置
其实原理都是一样,只需要配置多个入口文件,输出多个html文件即可
const pages = fs.readdirSync(path.resolve(__dirname, 'src/page'))
const multiEntry = pages.map((page) => {
const key = page.split('.js')[0]
return{
[key]: path.resolve(__dirname, 'src/page', page)
}
})
const mutilHtml = pages.map((page)=> {
const name = page.split('.js')[0]
return new HTML_WEBPACK_PLUGIN({
template: path.resolve(__dirname, './src/tepmlate/index.html'),
title: 'webpack-config',
filename: `${name}.html`,
chunks:[name]
})
})
module.exports = {
entry: multiEntry,
plugins:[
...mutilHtml
]
webpack-dev-server
"start": "webpack-dev-server --config ./webpack.dev.js",
devServer: {
contentBase: path.resolve(__dirname, "build"),
compress: true,
port: 9000,
open: true,
hot: true,
hotOnly: true,
proxy: {
'/api': 'http://www.xxx.com'
}
我们在本地开发的时候,可以使用webpack-dev-server为我们开启一个服务器,来实现,模块热更新、代理、压缩等服务器特性。具体使用,参照官网www.webpackjs.com/configurati…
结语:
本文列举了在开发中我们常遇到的一些问题,以及如何解决这些问题的webpack简易配置,这只是一个模板,具体的配置,我们还是得参照具体的业务,这里只给大家提供一个门路,希望大家在开发的时候,遇到问题尽可能的往这方面去想一想,没准就能够解决你的燃眉之急。