说说为何项目需要使用webpack打包

一、项目开发结构的演变

1、传统前端项目

传统前端项目中,使用引用script和link css来关联脚本和css文件,然后使用跳转链接来关联项目中的其他页面,从而形成整个网站访问的结构树。

2、依赖es6 module能力的模块化项目

当转变为模块化项目 时,可以在<script type="module">中通过import引入其他module的js。

仅仅依靠js原生esm的module功能,

能实现:

  1. js分模块,通过导入来关联

不能实现:

  1. 把html的内容分模块,然后组合成一个完整的html
  2. css也没法分模块
  3. 文件名称必须时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);

解析:

  1. 把每个模块处理成一个函数,这样各个模块作用域之间就不会存在变量冲突
  2. 打包出来的js文件就是一个立即执行函数,也不会污染全局变量
  3. 从入口模块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',
          },
        ],
      },
    ],
  },
};

解析:

  1. entry入口'./src/main.js'
  2. output.path 打包位置dist
  3. clean.keep: /.html$/ 不要清除dist中的html文件
  4. module.rules[0] ,模块匹配条件:test('.scss'), 匹配上之后使用use两个loader处理(css-loadermy-sass-loader
  5. 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-loadermy-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

解析插件要做什么:

  1. public/index.html文件读取出来
  2. 把webpack生成的css文件,自动注入到public/index.html中(需要用到cheerio
  3. 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的三个环境

  1. webpack本身代码的运行环境(node),连同webpack.config.js,各种插件都是在编译时的node程序中运行
  2. webpack以及插件编译过程的文本语法解析环境(项目中的源码,会被解析和转译,比如源码中的import语句,不是真正执行,而是处于编译环境中,被语法分析)
  3. 打包后的输出代码的运行环境(一般就是浏览器运行环境,在webpack编译过程中并不涉及最终运行环境,但是生成的代码的目标是运行在最终运行环境,所以es6转es5等等很多操作都是基于这个目标环境来编译的)

八、原理过程

  1. webpack根据依赖关系,递归从a找到b,从b找到c,解析每一个依赖(重复的不会再次解析)
  2. 解析每一个依赖,根据loader的匹配规则,匹配上的就交由loader转译(把所有其他什么jsx、ts、css等等,转译成webpack认识的js)
  3. 在整个生命周期的过程中,放置很多钩子,触发时调用插件plugin的对应方法(可以复制静态文件,分离文件,等等操作)
  4. 代码压缩、等等各种转换代码、转换文件的操作,最终得到想要的目标输出文件

九、webpack最佳实践(想要达到的目的)

  1. 使用vite,开箱即用,不用自己配置
相关推荐
理想不理想v13 小时前
webpack最基础的配置
前端·webpack·node.js
臣妾没空15 小时前
全栈里程碑二:前端基础建设
webpack
Domain-zhuo20 小时前
如何利用webpack来优化前端性能?
前端·webpack·前端框架·node.js·ecmascript
初学者7.20 小时前
Webpack学习笔记(2)
笔记·学习·webpack
理想不理想v20 小时前
webpack如何自定义插件?示例
前端·webpack·node.js
森叶1 天前
【附源码】Electron Windows桌面壁纸开发中的 CommonJS 和 ES Module 引入问题以及 Webpack 如何处理这种兼容
webpack·electron
初学者7.1 天前
Webpack学习笔记(3)
笔记·学习·webpack
Byron Loong1 天前
Python+OpenCV系列:【打卡系统-工具模块设计】工具模块深度揭秘,考勤智能化的核心秘籍!
python·opencv·webpack
初学者7.2 天前
Webpack学习笔记(4)
学习·webpack
理想不理想v3 天前
免登陆是什么?
服务器·前端·javascript·vue.js·webpack