优化(学习)公司的webpack配置(一)

蒋岳
2023-12-01

优化(学习)公司的webpack配置

2021年遇到的项目中,我遇到了一些特别难受的问题:

  1. webpack 编译慢,多达50多秒( 我觉得是文件结构深,很多文件冗余的原因)
  2. 打包体积大,优化过后还有90M
  3. CI/CD慢,一个小bug,因为CI/CD慢,甚至半天一天久过去了(当然也没有一把过的原因)
  4. 项目首屏加载慢
  5. node_module 体积超级大,达到惊人的1.4G

昨天,就是3.23号,我试着优化了node_module,经过一天的努力,我决定还是先不要搞这个问题,但是也不是一无所获。

导致node_module 过大的原因主要有几点
1、 循环依赖,比如module_A 依赖module_B,而module_B依赖module_C…

2、 npm 策略有缺陷,多个依赖都依赖module_A,但是在不同的层级中都下载了module_A,没有重复使用 module_A

尝试的解决方法:

使用pnpm:

​ pnpm 可以把依赖树扁平,使得多个依赖共同使用,不用重复下载。

结果:

成功把1.4G 的 module优化到了400多M, 大大的提高了项目的初次启动工作,减少了极其繁琐的pakege安装…在启动项目的时候,发现有些依赖没有下载到,所以项目根本启动不了。

2022.03.23结


今天,3.24号,我把目光放在了webpack的配置里面

背景:公司使用自己封装的webpack打包工具(可以理解为 webpack-tool),这个工具把根据公司通用的项目结构写的, 并把这个发布到了公司自己的npm 仓库中。这样有好处先不谈,缺点就是难于自定义配置。

原来的项目打包只需要

 "dev": "cross-env-shell NODE_OPTIONS=--max_old_space_size=4096 LANGUAGES_ENV=zhCN ENV=loc NODE_ENV=development npx webpack-tool run -m dev",

开始操作:

安装webpack 和 webpack-cli 并且运行

// 安装
npm install webpack webpack-cli –g
// 运行
npx webpack 

因为没有设置webpack的mode,所有发生警告,但是问题不大,webpack 会默认按生产模式打包,同时人口文件默认是index.js ,默认输出打包文件在dist 目录下。

新建webpack.config.js

在根目录下,与src 同级别,新建一个webpack.config.js 文件(文件名规定这样),webpck运行的时候,会自动根据这个文件里的配置进行打包

webpack.config.js

module.exports = {
 mode:'development', // development, production
 entry:'./src/index.js' // 入口文件
 output: {
    // webpack 如何输出结果的相关选项
    path:path.resolve(__dirname, "dist"), // string (default)
    // 必须是绝对路径
    filename: "bundle.js" // string (default)
 }
 module:{ // 配置loader
 	rules:[
        
 	]
 },
 resolve:{
 	alias:{ // 设置别名
 	
 	}
 },
 plugins:[ // webpck 插件
 
 ]

两个插件

webpack 打包过程中,会触发很多的钩子函数,webpack 的插件,本质上就是一个构造函数,并且在函数内部了定义了webpack 钩子函数触发时要对项目进行的处理,比如我们想要在 react.js 在生命周期componentDidMount里要做什么

介绍两个比较有用的插件

html-webpack-plugin

该插件将为你生成一个 HTML文件,会在 body 中使用 script 标签引入 webpack打包生成的 bundle.js。 只需添加该插件到你的 webpack 配置中,就不用手动添加。而且成都的html文件在内存中,减少了磁盘的读写。

webpack-dev-server

你启动webpack-dev-server后,
你在目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存当中。因此很多同学使用webpack-dev-server进行开发的时候都看不到编译后的文件

npm install --save-dev html-webpack-plugin
npm install --save-dev webpack-dev-server 

如果不使用HtmlWebpackPlugin,那么就需要在使用bundle.js(webpack打包好之后的js文件) 的html文件中使用src的方式引入

index.html

<script src="dist/bundle.js">

使用 HtmlWebpackPlugin之后,webpack 会自动把打包好的文件引入对应的html 模板文件,不需要手工操做,让我们的双手可以解决另外的问题

webpack-dev-serve 可以帮我们启动服务,不然我们就需要在每改一次代码,删掉dist 文件夹,重新打包,在打开index.html

使用webpack-dev-serve 之后,就可以设置端口,启动热更新等的

https://webpack.docschina.org/plugins/html-webpack-plugin/

https://webpack.docschina.org/api/webpack-dev-server/#Installation

webpack.config

// 配置 在webpack.config.js 的plugins 里面
plugins: [
	new webpack.HotModuleReplacementPlugin(), // 启动热更新第三步: 
    new HtmlWebpackPlugin({
        template: './src/index.html', // 编译模板
        title:"会员经营平台", // 等等其他参数
    }),
]

新建文件 Serve.js

const Webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const webpackConfig = require('./webpack.config.js');

const compiler = Webpack(webpackConfig);
const devServerOptions = {
    	open: false,   // 打开浏览器
		port: 3001,    // 端口号
		static: './src/index.html',
		hot: true,        //启动热更新第一步
		compress: false,
};

//  WebpackDevServer输入两个参数
const server = new WebpackDevServer(devServerOptions, compiler);

server.startCallback(() => {
  console.log('Successfully started server on http://localhost:8080');
});

把pakage.json 文件中公司以前的命令修改为自己的

// 原来的
"dev": "cross-env-shell NODE_OPTIONS=--max_old_space_size=4096 LANGUAGES_ENV=zhCN ENV=loc NODE_ENV=development npx webpack-tool run -m dev",

// 修改为
"dev": "NODE_ENV=development  LANGUAGES_ENV=zhCN ENV=loc NODE_ENV=development node ./Server.js",

那么以后的启动命令就可以是

npm run dev

总结:

  1. webpack-dev-server 给我们起了一个服务

  2. html-webpack-plugin 在内存中生成了一个html文件,并把打包的好的bundle.js 用script的str引入

  3. 在有服务,并且服务启动的前提下,才可以利用webpck本身的一个插件启用热更新。

    new webpack.HotModuleReplacementPlugin(),


超级磨人的 babel-loader

开始我们的第一个loader前:什么是loader?为什么要用loader?

因为webpack 会把入口文件(index.js),以及入口文件所有引用的文件,全部打包成一个js文件(出口文件bundle.js)。但是webpack 只能理解普通的js 文件,不能理解ES6 的语法,所以需要把高级语法,转为webpack 可以理解的es5语法的javascript.

babel-loader 这是一个很强大(磨人)的loader ,并且需要和其他的插件一起使用

// 安装babel以及其核心依赖 两者版本需要对应 
npm install --save-dev babel-loader@^7.1.5 babel-core@^7.0.0-bridge.0 

// @babel/preset-env  替换babel-preset-es2015,转译语法,const let 箭头函数等
npm install --save-dev  @babel/preset-env

// @babel/plugin-proposal-class-properties 编译ES6 class类语法
npm install --save-dev @babel/plugin-proposal-class-properties

// plugin-transform-runtime 
npm install --save-dev plugin-transform-runtime

// @babel/plugin-proposal-decorators  编译装饰器的语法
npm install --save-dev @babel/plugin-proposal-decorators

//  @babel/preset-react 编译react的jsx 语法
npm install --save-dev  @babel/preset-react

// babel-plugin-transform-imports 模块化导入 按需引入 预防变量全局污染
npm babel-plugin-transform-imports

配置

 module:{
 	rules:[
        {
            test: /\.(js|jsx)$/,
            exclude: /(node_modules)/, // 编译的时候排除这个文件夹
            use: {
                loader: 'babel-loader',
                options: {
                    presets: ["@babel/preset-env","@babel/preset-react",],
                    plugins: [
                        //执行的顺序是从右向左,所以基础插件要放在最后
                        ["@babel/plugin-proposal-decorators", { "legacy": true }],
                        ["@babel/plugin-proposal-class-properties"],
                        ["@babel/plugin-transform-runtime"],
                        [require('babel-plugin-transform-imports'), {
                            "my-component": {
                                transform: function(importName, matches) {
                                    let newPath =`my-component/component/${importName}`
                                    return newPath
                                },
                                preventFullImport: true
                            },
                             lodash: {
                                transform: "lodash/${member}",
                                preventFullImport: true
                            }
                         }
                       ]
                    ]
                }
            }
         }
 	]
 }

总结:

  1. babel-loader 依赖 babel-core,两者的版本很重要,即便搞过几次,但是在版本的不对的时候,还是花 了好写时间处理,所以在下载依赖的时候,还是把版本也加上吧

  2. babel/preset-env 可以对语法进行编译,编译成浏览器可以给理解的javaScript,可以指定浏览器和node的版本,制定兼容什么版本,也是一个可以减少代码体积的方案。

  3. babel/plugin-proposal-class-properties 可以对class类进行编译,{ loose: false },此参数默认false ,在编译class 累的时候,赋值声明等是用" = ",如果设置了false,那么就用使用 Object.defineProperty() 解析claas。

    babel https://babeljs.io/docs/en/assumptions#setpublicclassfields

  4. babel/plugin-proposal-decorators 是用装饰器的时候需要编译把装饰器语法编译为普通的语法

    // 案例一 antd 3.x,把from的api注入到组件中
    import { Form } from "antd";
    class DemoDetails extends React.Component {
        ...something
    }
    export default Form.create()(DemoDetails);
    
    

    如果是只用装饰器那么就可以这样子写,并且我们可以把其他的方法也可以通过装饰器的方式主注入到组件的props中,如果没有装饰器语法,那么可以想象要嵌套多少层调用。

    虽然有点跑题,但是总的来说,如果要是用装饰器,那么就等是把装饰器语法编译为普通的语法,就需要使用这个插件

    import { Form } from "antd";
    import { withRouter } from 'react-router-dom';
    import { observer } from 'mobx-react';
    
    @Form.create() // 注入antd 的form的api
    @withRouter    // 注入后可以在props中拿到router 对象
    @observer  // 给组件添加订阅
    class DemoDetails extends Component {
        ...something
    }
    export default DemoDetails
    
    
  5. plugin-transform-runtime 对引入的对象和全局变量是完全隔离。会把我们声明的变量前面加一个_比如_userName

  6. babel/preset-react把jsx 编译为classCreaat() 语法,就是处理react.js的

  7. babel-plugin-transform-imports 可以帮我们按需引入组件

    src\my-component\index.js

    module.exports = {}; // 只有空空的一个文件夹
    

    src\my-component\conponent文件夹下面有很多的组件

    src

    -my-component

    ​ -omponent

    ​ -MyButton

    ​ -MyForm

    ​ -MyTable

    ​ -inde.js

    那么如果要使用组件MyTable, 那么需要使用MyTabl组件就需要把完整的路径写完,但是我们可以使用babel-plugin-transform-imports帮我。

    // 使用前
    import MyTable from 'my-componet/component/MyTable'
    // 使用后
    import {MyTable } from 'my-componet'
    

    当Babel遇到import的时候会怎么样呢

    import { Grid, Row, Col } from 'react-bootstrap';
    

    babel 会简单的编译为下面的样子,这样就会把整个文件加载进来。

    var reactBootstrap = require('react-bootstrap');
    
    var Grid = reactBootstrap.Grid;
    var Row = reactBootstrap.Row;
    var Col = reactBootstrap.Col;
    

    这个babel 的插件可以帮我们自动把导入编译为一下模式,这样就可以大大的减少了打包体积了

    import Grid from 'react-bootstrap/lib/Grid';
    import Row from 'react-bootstrap/lib/Row';
    import Col from 'react-bootstrap/lib/Col';
    

    https://segmentfault.com/a/1190000010787241


第二个loader

webpack 只会处理的普通的js问价,是不能理解css,less 的,所以我们需要讲css编译成webpack 熟悉的js

安装

npm install --save-dev style-loader css-loader less-loader

配置

 module:{
 	rules:[
         ... other loader
         {
             test:/\.(js|jsx)$/,
             use:['style-loader','css-loader','less-loader']
          }
 	]
 }

注意的是,当有多个loader 共同处理时,loader 是顺序是从右往左的:less-loader 处理完得到css文件,再经过css-loadr 处理,style-loader 就可以使用了

配置全局css变量,配置主题等

themes.less

// 部分代码
@yx_yxHd:  2px;
@primary-color: #1861F2;
// @primary-color: #000; // 全局主色
@success-color: #52c41a; // 成功色
@font-size-base: 14px; // 主字号
@heading-color: rgba(0, 0, 0, 0.85); // 标题色
@text-color-secondary : rgba(0, 0, 0, .45); // 次文本色

other.less

// 不需要导入既可以使用变量:@yx_yxHd
//圆点/Circle
.yx_yxCircle_sm{
  width: 4 * @yx_yxHd;
  height: 4 * @yx_yxHd;
  border: 1px;
  box-sizing: border-box;
}

安装,配置

// 安装 less-vars-to-js,可以把文件,转为js的对象形式
npm install --save-dev less-vars-to-js
/* 配置 */ 
const fs = require('fs')
const lessToJs = require('less-vars-to-js');

// 读取less 文件
const paletteLess = fs.readFileSync(path.resolve(__dirname, "src/themes/themes.less"), 'utf8');

// 转为js 对象
const baseLess = lessToJs(paletteLess, {
	resolveVariables: true,
	stripPrefix: false
});
 
// 配置 webpack.config.js
 module:{
 	rules:[
      ... other loader
      {
        test:/\.(js|jsx)$/,
        use:[
            {
                loader: 'style-loader',
            },
            {
                loader: 'css-loader',
            },
            {
                loader: 'less-loader',
                options: {
                    lessOptions: {
                        strictMath: true,
                        modifyVars: lessVars,// less 变量
                        javascriptEnabled: true,
                    },
                },
            },
    	  ]
       }
 	]
 }

设置别名

resolve: {
    alias: {
        src: path.resolve(__dirname, 'src'),
        utils: path.resolve(__dirname, 'src/utils'),
        config: path.resolve(__dirname, 'src/config'),
        my-conponent: path.resolve(__dirname, 'src/conponent'),
        assets: path.resolve(__dirname, 'src/assets'),
        router: path.resolve(__dirname, 'src/router'),
        themes: path.resolve(__dirname, 'src/themes'),
        pages: path.resolve(__dirname, 'src/pages'),
    }
},

从开始把目光放在了node_module 文件上,到把精力投入到webpck 中,可以说,没有那一分钟事没有遇到过问题,都是在不断的看博客,找webpack 官网等,有的问题已经搞定,但是也有的花了很多的时间也没有处理好。

问题一

按照我的理解,如果我在运行一面的这一命令,那么我就可以在项目中根绝着几个变量,配置项目的

"dev": "NODE_ENV=development  LANGUAGES_ENV=zhCN ENV=loc NODE_ENV=development node ./Server.js",

而我在webpack .config.js 文件去没有拿到对应的变量

console.log("webpackConfigJs:",NODE_ENV,LANGUAGES_ENV,NODE_ENV); // ReferenceError: NODE_ENV is not defined
console.log("webpackConfigJs:",process.env);  // 没有发现对应的变量
console.log(process);  // 还是没有发现

webpack 还挺有趣,下周如果原型还没有出来的话,继续优化这个项目

 类似资料: