前言:前端模块化发展过程
- 1:web1.0时代,没有模块化的概念,采用命名空间的形式隔离模块之间的命名冲突,可以理解为一个文件就是一个挂载在window下的对象,其实也无法避免命名冲突
- 2:接着引入了IIFE自执行匿名函数,每一个模块封装为一个自执行的IIFE,将依赖项作为IIFE的参数传入,避免全局变量的冲突。但是这样还是需要开发人员手动的处理js文件之间的依赖关系,在用script标签引入文件顺序时,需要开发手动的调整。
- 3:为了解决js文件之间的依赖需要手动维护的问题,引入的AMD的模块化方案,最出名的require.js这个模块加载器 先看下采用AMD模块化方案的使用方式
一:定义一个模块
/**
* 第一个参数为模块的名字:alpha
* 第二个参数为当前依赖的模块集合:"require", "exports", "beta"
* 第三个模块为一个函数,函数的形参与第二个参数的依赖项一一对应。
* return:返回模块暴露的属性
*/
define("moduleA", ["require", "exports", "beta"], function (require, exports, beta) {
return {
verb:function(){
return alpha.verb() +2;
}
};
});
二:使用一个模块
/**
* 第一个参数:一个数组存放当前文件所有依赖的模块
*/
require(['moduleA'], function(moduleA) {
console.log(moduleB);
});
但AMD模式有以下几个问题
- 问题一:写法上比较繁琐和冗余 每定义一个模块需要声明模块,传入模块依赖项集合,再将模块依赖项作为形参一一对应的传给第三个回调函数,每使用一个模块。也得先require后再作为形参一一对应的传递给第二个回调函数。
- 问题二:如果模块的颗粒度划分的比较小,就会导致页面有很多的js请求
- 4:给予AMD存在的问题,最后也就出现了我们现阶段浏览器端的esModules和nodejs端的commonjs方案。(注:当然现在nodejs也支持esModules了,只需我们在运行的时候在package.json中声明以下,或启动nodejs的时候添加对应参数)
基于es2015中esModules的模块化方案,也就推出了现在的工程化打包工具webpack。(当然webpack不止支持esModules)。
webpack是如何分析这些模块的依赖关系的呢?
- 1:首先我们通过执行webpack-cli -options webpack.dev.config.js。执行webpack.dev.config.js这个文件。为webpack执行的配置文件。
- 2:首先webpack会从配置文件的entry中指明的入口文件进行匹配,AMD,CMD,esModules,commonjs导入的文件,自动生成文件的依赖图(也就是文件之间的模块依赖图,省略了手工声明依赖关系)。
- 3:对于webpack来说一切皆模块,但也不是所有的模块都能解析的。所以对于css,scss,less或者是图片,pdf等等。需要通过对应的loader来解析对应的文件,并转化为webpack可识别的模块。通过loader处理的模块被webpack加载最后添加到整个项目的文件依赖图中。 接下来我们分析下怎么处理css
- 1:首先我们需要使用cssLoader来处理css文件,将css文件转为webpack可识别的css模块,添加的文件的依赖图中
- 2: cssLoader处理后的模块只是能被webpack加载,但这时还不会有样式上的效果.
- 3: 要有样式的效果还需要我们引入styleLoader将css添加到style标签中,才会生效
- 4: 当然这时候也可加入postcssLoader来处理css在不同浏览器上的前缀,做到更充分的兼容
- 5: 在也可以
MiniCssExtractPlugin.loader将css抽取到一个文件 接下来我们以sass为例看下css相关loader的配置
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
ignoreOrder: false,
}),
],
module: {
rules: [
{
test: /\.scss$/,
// 注意以下use中的normal loader,执行顺序时由下往上执行的。
// 另外的pitch loader的执行顺序则由上到下,再由下到上。类似DOM的事件捕获和事件冒泡之间的顺序。也可以理解为和洋葱模型的执行顺序很相似。
use: [
MiniCssExtractPlugin.loader,
{
loader: 'style-loader',
options: {}
}
{
loader: 'css-loader',
options: {
// 在css中遇到@import语句,就回退两个loader,用sass-loader,postcss-loader继续处理这个import进来的scss文件。
// 当然如果能确保@import引入的只有css文件,不会再引入scss文件。那可以将importLoaders: 1,之后在css中遇到@import的文件,只会回退到'postcss-loader'处理就可以了
importLoaders: 2,
}
}
{
loader: 'postcss-loader',
options: {
plugins: [
require('autoprefixer') // 需要在项目根目录中创建了 `postcss.config.js`
]
}
}
{
loader: 'sass-loader',
options: {}
}
]
}
]
}
}
webpack搭建工程化架构,在打包时如何将js和css自动兼容到对应版本的浏览器。
-
1:在安装webpack时会默认安装browsersList,browsersList会依据项目中 .browserslistrc文件(或者是package.json中browsersList字段)中的配置。browsersList会到caniuse上,自动获取需要支持的浏览器列表。(至于如何获取,可以理解为browsersList向caniuse发送了一个请求获取支持的浏览器列表)
-
2:如何配置.browserslistrc呢?一般我们只需要在项目跟路径创建一个.browserslistrc文件。
last 2 version # 兼容最近的两个版本
> 1% # 兼容市场占有率大于1%的浏览器
maintained node versions
not dead # 支持市场还在持续维护的浏览器
若是不配置的话则采用以下默认配置
0.5% # 市场占有率大于0.5%的浏览器
last 2 versions # 兼容最近两个版本的浏览器
Firefox ESR
not dead # 兼容在持续维护中的浏览器
使用命令行执行npx browserslist就可以看到到.browserslistrc配置所有需要支持的浏览器列表了。
上面聊了browserslist获取到需要支持的浏览器的版本,那webpack搭建的工程是如何自动将css兼容到对应浏览器版本的呢。
- 这里就需要postcss来帮我们处理了,通过browserslist获取需要支持的浏览器,根据浏览器自动添加css前缀。
- 这时候就需要我们引入postcss来处理所有的css。需要我们通过postcss-cli手动的调用命令行行编译制定css文件添加前缀。
- postcss本身并不会帮我们添加前缀,需要我们引入
autoprefixer这样一个postcss插件,自动添加需要兼容浏览器的css前缀。
如果每次都需要我们通过postcss去处理每一个css文件这样就很繁琐了。
所以也就引入了postcss-loader来自动处理所有的css文件,并自动添加需要兼容的前缀
- postcss-loader内部使用的是postcss,postcss是使用
autoprefixer插件来自动添加前缀的。- 所以需要我们配置postcss-loader(注:postcss-loader配置在css-loader的下面或右边)来处理,并在options的plugins里,引入
autoprefixer。
{
loader: 'postcss-loader',
options: {
plugins: [
require('autoprefixer'), // 需要在项目根目录中创建了 `postcss.config.js`
require('postcss-preset-env'), // postcss-preset-env为postcss预设的插件集合。与babel-preset-env类似。
]
}
}
// 因为`postcss-preset-env`依据预设了postcss的很多插件
// 我们可以拿来即用,不用关注内部需要使用那些插件, 所以我们可以改写为以下方式
{
loader: 'postcss-loader',
options: {
plugins: [
require('postcss-preset-env'), // postcss-preset-env为postcss预设的插件集合。与babel-preset-env类似。
]
}
}
以上的这些postcss-loader的配置,都可以放到postcss.config.js中配置。这样postcss-loader会默认从项目的根目录读取postcss.config.js的配置。便于项目中配置的复用
// postcss.config.js
module.export = {
plugins: [
require('postcss-preset-env')
]
}
使用postcss.config.js作为项目全局的postcss-loader配置使用
// postcss.config.js写好后我们只需要在项目中,按下面这样配置我们的postcss-loader即可。
// postcss-loader会默认去读取postcss.config.js中的配置
{
loader: 'postcss-loader'
}
那么问题来了,因为postcss-loader遇到css中的@import语句并不会处理@import所引入的文件,在css中如果我们遇到了@import语句后怎么办呢?
需要
importLoaders来处理
{
loader: 'css-loader',
options: {
importLoaders: 2, // importLoaders的value为数字num,表示在css中遇到@import语句,就从之前的num个loader开始,再次解析这个@import进来的文件。
// css里background:url();url引入的文件不以esModule的方式导入,直接导入文件本身
// 因为在css-loader中遇到url引入的图片,会替换为require()引入图片,所以读取的时候只能是require().default获取图片,显然css中不能这样写。所以设置esModule: false,。
// 在css中不以esModule的方式导入,直接获取图片文件。
esModule: false,
}
}
webpack搭建工程化架构,在打包时如何处理图片,pdf或字体等二进制文件呢?
- 1 需要引入
file-loader来处理这些模块(注:file-loader虽然能处理图片,但图片的处理我们一般用url-loader,因为url-loader能将小文件转为base64编码的图片内嵌到我们的css或html中,以达到减少网页请求数量的目的)
// file-loader的配置
// 匹配pdf
{
test: /\.pdf$/,
use: {
loader: 'file-loader',
options: {
// 引入的模块不转为esModule,直接引入文件本身
esModule: false,
outputPath: 'dist/asset/', // 打包指定资源的输出目录
}
}
}
// 匹配字体
{
test: /\.(ttf|woff2?)$/,
use: {
loader: 'file-loader',
options: {
// 引入的模块不转为esModule,直接引入文件本身
esModule: false,
outputPath: 'dist/asset/', // 打包指定资源的输出目录
}
}
}
- 2 针对图片模块化加载,使用
url-loader(注:url-loader默认会将所有的图片都转为base64编码的图片内联到html中)处理。
// url-loader的配置
/* url默认会将所有的图片转为base64,所以在options里可以限制limit,
* 体积在limit内的图片转为base64编码的图片。而大于limit限制的图片url-loader内部
* 会调用file-loader来拷贝这个文件,将文件的地址放到html的内联属性上
*/
{
test: /\.(png|svg|gif|jpe?g)$/,
use: {
loader: 'url-loader',
options: {
name: '[name].[hash:6].[ext]', // 打包输出图片的名称
limit: 2 * 1024,
outputPath: 'dist/img/', // 打包后img的输出路径
}
}
}
因为webpack默认只识别js文件,所以使用loader用来转换特定类型的文件,帮助webpack识别和加载这些文件,形成打包的依赖图。
上面聊了loader,那webpack plugin主要处理什么呢?
因为loader的执行是在模块加载时执行的,那我们知道其实webpack打包时时有自己的一些自定义事件的,plugin基于webpack的事件机制,能让我们的plugin在webpack打包流水线上的任意时机执行。比如
- 可以在打包开始的时候执行plugin。
- 可以在打包过程中的某个事件执行。
- 可以在文件落地磁盘前执行某些操作。 因为loader只在模块加载时执行,无法参与到这些过程中,所以引入了webpack plugin来处理的这些时机的文件。
接下来聊聊webpack常见的插件配置
- 1:
clean-webpack-plugin,在每次打包时都会情清空dist目录内的文件
// clean-webpack-plugin配置
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
plugin: [
// 也可以传入配置参数,清空dist下指定类型的文件。配置的文档地址:[https://www.npmjs.com/package/clean-webpack-plugin](url)
new CleanWebpackPlugin();
]
- 2:
html-webpack-plugin,在每次打包时帮我们自动注入数据到html中,避免每次打包都需要手动修改html。
const HtmlWebpackPlugin = require('html-webpack-plugin');
// HTML模板使用ejs语法,写上占位符。在HtmlWebpackPlugin中传入数据可以进行插入对应占位符。
plugin: [
new HtmlWebpackPlugin({
title: 'webpack app',
template:'src/public/index.html',
});
]
- 3:
webpack.DefinePlugin,在每次打包时注入全局的常量
const { DefinePlugin } = require('webpack');
// HTML模板使用ejs语法,写上占位符。在HtmlWebpackPlugin中传入数据可以进行插入对应占位符。
plugin: [
new DefinePlugin({
BASE_URL: '"./"'
envConfig: JSON.stringify({ mode: 'development' })
});
]
- 3:
copy-webpack-plugin,每次打包拷贝静态资源到目标dist目录
const CopyWebpackPlugin = require('copy-webpack-plugin');
plugin: [
new CopyWebpackPlugin({
patterns: [
{
from: 'src/public/',
to: 'dist/css/', // to不写的话,则默认拷贝到dist目录下
globOptions: {
ignore: ['**/index.html'], // 忽略public下的index.html文件,这个文件不拷贝
}
}
],
});
]
上面的loader介绍也好,plugin的介绍也好,本质上都是对js之外的文件资源进行处理。
webpack如何处理js的呢?
首先想到的肯定是babel,babel依赖的@babel/core来解析我们的代码。但如果只通过@babel/core解析es6+的代码。那么编译后的代码是没有变化的。@babel/core依赖的是babel语法转换插件帮助我们转换语法。
- 因为babel使用的是插件机制,每种需要转换的语法都需要我们提前安装对应的语法转换插件,如@babel/plugin-transform-arrow-fucntions插件来转换箭头函数。但是通过开发人员手动安装所有的语法转换插件太过繁琐。
- 所以
官方已经将大部分的es6+语法转换插件预设到@babel/preset-env中。我们开发的时候只需要安装和引入@babel/preset-env就可以使用es6+语法了。-->(这个其实和postcss的postcss-preset-env很像,postcss也是把常用的插件预设到postcss-preset-env中了,我们开发的时候只要引入postcss-preset-env即可)。- 但对于很对还在实验性阶段的es语法,还是不会预设到
@babel/preset-env中的,所以想要使用最新的实验性es语法,还是需要手动安装引入对应语法的babel转换插件。
那在webpack中我们可以引入babel-loader来处理js。
// babel-loader的使用
{
test: /\.js$/,
use:[{
loader: 'babel-loader',
exclude: '/node_modules/'
options: {
preset: [
// 引入babel插件集合,但这样使用的话。万一项目中其他地方还要配置babel-loader,还需要将这个options拷贝过去,所以我们可以和postcss类似。在项目根目录新建一个babel.config.js,之后项目中所有的babel-loader都从babel.config.js中读取配置项
[
'@babel/perset-env',
// 声明babel转换后的代码需要兼容到哪些浏览器,如果没有配置target。babel则默认会兼容到browsersList中的浏览器版本。所以项目中配置的browsersList,在babel中我们不需要配置target这个参数。所以注释掉这个配置
// {
// target: 'chrome 80'
// }
]
]
}
}]
}
创建babel.config.js作为项目全局的babel-loader配置使用.
// 当前文件:babel.config.js,新建在项目根目录
module.export = {
presets: {
[ '@babel/presets-env' ]
}
}
// babel.config.js写好后我们只需要在项目中,按下面这样配置我们的babel-loader即可。
// babel-loader会默认去读取babel.config.js中的配置。
// 此时的babel-loader配置如下
{
loader: 'babel-loader'
}
从postcss-loader和babel-loader的配置可以看出什么共同点吗?
- 1:都已browserslist作为兼容判断条件的条件
- 2:都是插件机制,需要对应的兼容转换时都需要引入和使用对应的插件,这样做都比较繁琐和冗余
- 3:
于是postcss和@babel官方都各自抽取了postcss-preset-env和@babel-presets-env的插件集合,支持大部分标准的css和js的兼容。 - 4:但是在每个postcss-loader或@babel-loader中配置对应的preset-env,代码结构又嵌套比较深,也不好复用。
于是postcss和@babel官方各自定义了postcss.config.js和babel.config.js。只要在项目根目录建了这两个文件,之后postcss-loader或@babel-loader都会到对应的配置文件中读取配置。达到整个项目中的postcss和@babel的配置复用。 - 注意:如果在根目录之下的其他路径也新建了
babel.config.js。在这个文件目录之下,@babel就会以这个新建的babel.config.js的配置为依据编译代码,可以将babel.config.js理解为是一个具有作用域的配置文件
上面我们聊了babel如何转换es6+语法的配置,那es6+的api该如何转换呢?
@babel7之前我们可以直接通过引入@babel/polyfill来兼容所有已成为标准的es6+新特性。但这样直接引入太过粗暴,很多我们没有用到的特性对应的ployfill也被引入了,导致代码体积变大。
所以在@babel7之后就支持按需引入@babel/polyfill了,那在@babel7之后我们怎么按需引入polyfill了呢?
- 1:需要我们安装@core-js3,(官方不建议继续使用@core-js2,因为@core-js2将要停止维护了)
- 2:需要我们安装regenerator-runtime,来支持Promise,async await等语法。
那如何使用呢?
// 当前是babel.config.js文件
module.exports = {
presets: [
[
'@babel/presets-env',
{
// 有3个值false(不对当前js文件做polyfill填充),usege(根据我们代码中用到的es6+特性及需要兼容browserslist的版本按需的进行polyfill填充), entry(只根据需要兼容browserslist版本的浏览器进行polyfill,不管源代码中有没有用到es6+新特性),默认false。
useBuiltIns: "usage",
corejs: 3, // 这里必须指定corejs为3,否则会默认寻找corejs2,这样打包的时候就会报错。corejs3支持的es6+特性也更多
}
]
]
}
注意:如果选择了useBuiltIns: "entry";就需要在项目的入口js文件中引入"core-js/stable"和"regenerator-runtime/runtime"。
// 当前为入口js文件
import "core-js/stable";
import "regenerator-runtime/runtime";
一般在工作中我们使用的也就是useBuiltIns: "usage", corejs: 3,来进行@babel/polyfill的按需加载,减少我们代码打包后的体积。
到了这里在webpack中babel的配置也就结束了。接下来我们介绍下webpack提供的其他的功能
如何引入webpack的热更新功能
- 1:webpack热更新功能依赖于webpack-dev-server,在server中配置hot为true。webpack的配置如下
module.export = {
mode: 'development',
devtool: false,
entry: './src/main.js',
output: {
filename: 'js/main.js',
path: path.resolve(__dirname, 'dist),
},
target: 'web', // 开发阶段需要不需要考虑兼容浏览器,target为web,不依赖browserslist
devServer: {
hot: true,
publicPath: '×××', // 静态资源打包后的cdn域名和前缀path
}
}
- 2:代码的入口文件配置
// 入口文件main.js
if(module.hot) {
// app.js下所有的改动都支持热更新
module.hot.accept(['./app.js'], () => {
console.log('热更新了')
})
}
webpack-dev-server与热更新引入后,如何让webpack支持react呢?
- 1:首先需要babel-loader处理jsx模块,
- 2:babel-loader使用babel插件@babel/preaet-react来处理jsx模块文件。 这样我们的webapck就支持react的jsx模块文件了。
- 3:但是react的热更新还依赖于其他的babel插件
react-hot-loader和@hot-loader/react-dom。
// babel-loader配置
{
test: /\.jsx?$/,
exclude: /(node_modules)/,
use: [
loader: 'babel-loader',
]
}
// babel.config.js配置
module.export = {
presets: [
['@babel/presets-env', {
useBuiltIns: usage,
corejs: 3,
}],
['@babel/preset-react'],
['react-hot-loader/babel']
]
}
// webpack.config.js
module.exports = {
alias: {
'react-dom': '@hot-loader/react-dom',
}
}
// App.jsx
import { hot } from 'react-hot-loader/root';
function App = () => {
return (<div>
×××
</div>)
}
export default hot(App);
// mian.js
import APP from './app.jsx';
// 模块热替换的 API
if (module.hot) {
module.hot.accept();
}
基于上面的配置已经支持了react的jsx,在补充上react相关的热更新配置,那么一个基础的react配置就完成了。