一、项目开发结构的演变
1、传统前端项目
在传统前端项目中,使用引用script和link css来关联脚本和css文件,然后使用跳转链接来关联项目中的其他页面,从而形成整个网站访问的结构树。
2、依赖es6 module能力的模块化项目
当转变为模块化项目 时,可以在<script type="module">
中通过import
引入其他module的js。
仅仅依靠js原生esm的module功能,
能实现:
- js分模块,通过导入来关联
不能实现:
- 把html的内容分模块,然后组合成一个完整的html
- css也没法分模块
- 文件名称必须时html结尾(只能写原生3大件),无法适配各种各样的其他语法的模板内容,也就意味着没有其他能力(比如Sass,比如vue模板文件,其他工程化的内容)
3、工程化项目
随心所欲的使用各种模板语言,
比如Sass(通过Sass编译器转译为css后就可以整合到原始html中),
比如.vue
类型文件,通过vue的编译器解析就可以生成html,
二、原始前端项目工程化遇到的难题
但是,
工程化项目一般是依赖node环境,通过npm工具管理包依赖,
工程化项目中使用的各种语言都各自有自己的编译器(假设),难道手动去给每个编译器运行一下?
node中依赖的各种关系,离开了node环境发布到浏览器之后怎么运行?怎么把这些依赖关系转变为html或者原生js中可识别的代码?
node工程化项目极大的扩展了代码能力,比如可以通过引入babel插件在编译期间去polyfill es6的功能使得兼容es5,难道这个插件也要每次发布都自己运行一次吗?
有没有一种统一的方式,通过配置的方式,帮我们把以上所有这些功能都处理了?
让我们自由的开发,安心的部署?
接下来,就是webpack登场了!!!
三、webpack如何把工程化的多模块,转化为一个js文件
假如有a(入口模块)、b、c三个模块,有依赖关系,如何转化成js?
下面伪代码展示webpack打包合并基本原理:
js
(function(...abc){
//执行入口aFunc
aFunc(abc)
})(aFunc,bFunc,cFunc);
解析:
- 把每个模块处理成一个函数,这样各个模块作用域之间就不会存在变量冲突
- 打包出来的js文件就是一个立即执行函数,也不会污染全局变量
- 从入口模块aFunc开始执行,如果候aFunc内部依赖b模块,就会:
js
function aFunc(abc){
//a依赖b
abc.b();
}
上面描述了大概原理,下面是比较准确的代码解析:
第一步,把多个模块编译为多个函数,这样内部变量不会冲突
js
const 多个模块 = {
'./src/a.js': function (module, exports) {
console.log('module a');
module.exports = 'a';
},
'./src/index.js': function (module, exports) {
console.log(' index module');
var a = require('./src/a.js');
console.log(a);
},
};
第二步,模块依赖使用一个require函数表示,使用module变量接收模块的导出
js
function require(moduleId) {
var func = 多个模块[moduleId];
var module = {
exports: {},
};
func(module, module.exports);
}
第三步,一个立即执行函数,从入口模块开始执行
js
(function (多个模块) {
//执行入口模块
require('./src/index.js');
})(多个模块);
第四步,把前面三步 的代码合并为一个马上传参的立即执行函数,就不会存在任何全局变量:
js
(function (多个模块) {
function require(moduleId) {}// 函数写内部
//执行入口模块
require('./src/index.js');
})(直接传参:{多个模块});
四、在webpack中使用自定义loader
webpack.config.js
配置如下
js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main: './src/main.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
clean: {
keep: /\.html$/,
},
},
module: {
rules: [
{
test: /\.scss$/,
use: [
'css-loader',
{
loader: './loaders/my-sass-loader',
},
],
},
],
},
};
解析:
- entry入口'./src/main.js'
- output.path 打包位置dist
- clean.keep: /.html$/ 不要清除dist中的html文件
- module.rules[0] ,模块匹配条件:test('.scss'), 匹配上之后使用use两个loader处理(
css-loader
和my-sass-loader
) css-loader
是npm引入的包,my-sass-loader
是自定义自己写的loader
my-sass-loader
内容如下:
js
module.exports = function (source) {
const sass = require('sass');
let result = sass.compileString(source);
return result.css;
};
入口文件main.js
内容如下
js
import styleText from './assets/styles/index.scss';
var style = document.createElement('style');
style.innerHTML = styleText;
document.head.appendChild(style);
因为main.js
依赖index.scss
模块,index.scss
被webpack的test: /\.scss$/
后缀匹配,所以index.scss
会被css-loader
和my-sass-loader
处理转换。
这里是递归调用loader :
css-loader
pitch
my-sass-loader
pitch
my-sass-loader
css-loader
所以,先执行pitch,递归回来时再执行loader function,因此loader的执行顺序是倒过来的
上面,在my-sass-loader
中使用sass依赖包,把scss中的sass写法compileString
成了css格式,再交由css-loader
最后,在main.js
中,把转义后的styleText(css格式)添加到内部样式表《style》中
最后,在html中引用main.js
:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./main.js" defer></script>
</head>
<body>
<div id="app">这是一个将要应用样式的文字</div>
</body>
</html>
至此,一个简单的sass转义loader完成了!
更进一步:把sass文件转化成css文件
bash
pnpm install extract-loader file-loader --save-dev
js
const path = require('path');
module.exports = {
devtool: 'source-map',
mode: 'development',
entry: {
main: './src/main.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
// publicPath: 'dist/',
filename: '[name].js',
clean: {
keep: /\.html$/,
},
},
module: {
rules: [
{
test: /\.scss$/,
use: [
{
loader: 'file-loader',
options: {
name: 'assets/[name].css',
},
},
{
loader: 'extract-loader',
options: {
// publicPath: '../',
},
},
{
loader: 'css-loader',
},
{
loader: './loaders/my-sass-loader',
},
],
},
],
},
};
五、webpack中使用babel-loader把es6转成es5
babel是什么?
开始
安装babel
开发依赖:
pnpm install -D @babel/core @babel/preset-env
pnpm i core-js@3
babel
配置文件babel.config.js
:
js
module.exports = {
presets: [
[
'@babel/env',
{
targets: {
browsers: ['> 0.2% ', 'last 4 versions', 'not ie <=8'],
},
corejs: '3',
useBuiltIns: 'usage',
},
],
],
};
自定义一个loader:
添加到webpack.config.js
:
js
rules: [
{
test: /\.js$/,
use: [
{
loader: './loaders/my-babel-loader',
options: {},
},
],
},
],
my-babel-loader.js
内容:
js
const babel = require('@babel/core');
module.exports = function (source) {
let { code: result, map, ast } = babel.transformSync(source, {});
return result;
};
至此,自定义babel-loader调用babel-core能力转义es6为es5成功。
六、webpack使用自定义plugin,把第四
步中loader生成的css文件关联到index.html
安装html的node环境解析器: pnpm install cheerio -D
webpack.config.js中配置添加自定义的插件:
js
const MyPlugin = require('./plugins/MyPlugin');
module.exports = {
plugins: [new MyPlugin(null)],
}
项目目录结构如下:
arduino
项目
├── plugins
│ └── MyPlugin.js
│
├── public
│ └── index.html
│
├── src
│ ├── index.js
│ └── 。。。
│
├──webpack.config.js
解析插件要做什么:
- 把
public/index.html
文件读取出来 - 把webpack生成的css文件,自动注入到
public/index.html
中(需要用到cheerio
) - 把
public/index.html
文件写入到打包的output输出位置中
MyPlugin.js
内容如下:
js
const fs = require('fs');
const cheerio = require('cheerio');
const webpack = require('webpack');
module.exports = class MyPlugin {
constructor(param) {
this.param = param;
}
apply(compiler) {
//emit事件触发:打包,在内存中生成了文件,在写文件到磁盘之前
compiler.hooks.compilation.tap('compilation', (compilation) => {
const options = {
name: 'MyPlugin',
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
};
compilation.hooks.processAssets.tap(options, myPlugin_emit);
function myPlugin_emit(assets) {
let cssFilePathList = Object.entries(assets)
.map(([pathname, source]) => pathname)
.filter((pathname) => pathname.endsWith('.css'));
//读取index.html并解析成cheerio对象
const indexHtml = fs.readFileSync('public/index.html', 'utf8');
const $ = cheerio.load(indexHtml);
//添加css到index.html的link引用
cssFilePathList.forEach((cssFilePath) => {
$('head').append(`<link rel="stylesheet" href="${cssFilePath}">`);
});
const html = $.html();
//添加到output
compilation.emitAsset(
'index.html',
new webpack.sources.RawSource(html)
);
}
});
}
};
至此,dist目录下已经有一个关联了css文件的index.html
也可以使用style-loader把css样式注入到引用本js的html的head>style标签中
七、webpack的三个环境
- webpack本身代码的运行环境(node),连同
webpack.config.js
,各种插件都是在编译时的node程序中运行 - webpack以及插件编译过程的文本语法解析环境(项目中的源码,会被解析和转译,比如源码中的import语句,不是真正执行,而是处于编译环境中,被语法分析)
- 打包后的输出代码的运行环境(一般就是浏览器运行环境,在webpack编译过程中并不涉及最终运行环境,但是生成的代码的目标是运行在最终运行环境,所以es6转es5等等很多操作都是基于这个目标环境来编译的)
八、原理过程
- webpack根据
依赖
关系,递归从a找到b,从b找到c,解析每一个依赖(重复的不会再次解析) - 解析每一个依赖,根据
loader
的匹配规则,匹配上的就交由loader转译(把所有其他什么jsx、ts、css等等,转译成webpack认识的js) - 在整个生命周期的过程中,放置很多钩子,触发时调用插件
plugin
的对应方法(可以复制静态文件,分离文件,等等操作) - 代码压缩、等等各种转换代码、转换文件的操作,最终得到想要的目标输出文件
九、webpack最佳实践(想要达到的目的)
- 使用vite,开箱即用,不用自己配置