1

一、webpack的大致构建流程

  1. 初始化参数:在package.json读取对应的命令配置,得出最终参数
  2. 开始编译前的准备:实例化Compiler,加载所有plugin,执行对象的run方法并开始编译。接着根据配置中的entry找到所有入口文件
  3. 编译模块:从入口文件出发,调用所有loader对模块进行编译,再找到模块依赖,重复上述步骤知道所有入口文件都经过处理。在loader处理完所有模块后,得到的每个模块以及它们之间的依赖关系图
  4. 导出:根据入口和模块之间的依赖图,将代码组成一个个包含多个模块的chunk文件加入到输出列表,然后根据配置确定的输出路径和文件名,将chunk导出到项目目录中

流程图可以参考这里
webpack打包的规则是,一个入口文件对应一个bundle,该bundle包括了入口文件模块和其他依赖模块,按需加载的模块或者需要单独加载的模块则是分开打包生成其他的bundle。在这些bundle中,有一个较为特殊,就是manifest.bundle.js,被称作webpackBootstrap,它是最先加载的,负责解析webpack生成的其他bundle。

package.json

  1. npm init 初始化文件
  2. script里 可以 NODE_ENV=development 可以设置环境变量,默认是开发模式。

(我们就将NODE_ENV绑定到了process.env上,并且按以上配置,它所引入的脚本中访问到process.env.NODE_ENV。那我们为什么要设置这个?一般是用做环境判断 if(process.env.NODE_ENV ===xxx)
另外,使用cross-env可以实现设置时跨平台

  1. 安装依赖

webpack webpack-cli webpack-devserver

二、webpack总体配置

常用的配置

{
    🌺mode: development/production
    
    🌺entry:  【string | stringArray | object 可以多入口】源文件
            (如果只是string,默认打包后产物叫main.js, object的话产物为键名)
             
    🌺output: { // 输出
        path,
        
        filename: '[name].[chunkhash].js’, //  打包同步代码
        
            chunkFilename: 同上 // 打包异步代码( https://www.cnblogs.com/skychx/archive/2020/05/18/webpack-filename-chunkFilename.html)
            
            publicPath: '//7.url.cn/edu/act/' // 构建出的资源需要异步加载时,加载的cdn地址前缀(小心别写错了导致资源404)
            
            crossOriginLoading: 用于配置JSONP这个异步插入的标签的 crossorigin 值(如anonymous)
            
        library: 导出库的名字
        
            libraryTarget: 这个一般是封装第三方库时。🌰例如可以配置'commonjs2', 'umd’...
    }

    🌺module: { //【对象】如何处理模块,通过一堆loader
            noParse:不解析的部分。 // 如/moment(?!-)|node_modules\/chart\.js/, (忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能)
            
        rules: [ //【对象数组】
            {
                // ****条件匹配****
                test: 正则匹配文件名或正则数组。🌰例如 [ /\.jsx?$/,/\.tsx?$/],
                
                include: 包含哪些文件(路径)或路径数组
                🌰例如 [
                                       path.resolve(__dirname, 'src'),
                                       path.resolve(__dirname, 'tests'
                                 ],
                                 
                exclude: 排除哪些文件
                🌰例如 path.resolve(__dirname, 'node_modules') ,因为node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换。)
                 
                // **** 应用规则(里面的每一项就是一个loader)****
                use: [ //【字符串数组(则由后往前执行) | 对象数组】
                            {
                                    loader: ‘babel-loader’,
                                    
                                    options:{
                                      cacheDirectory:true,
                                   },
                                    
                                   enforce: 调节这个loader的执行顺序。
                                   🌰例如'post'把该 Loader 的执行顺序放最后
                            }
                 ]
            }
                  ]
    }
    
    🌺resolve: { // 翻译为解析,如何寻找模块
        modules: //【数组】解析第三方模块时应该搜索的目录列表  
            🌰例如 [
                path.resolve(rootDir, 'src'),
                path.resolve(rootDir, 'src', 'node_modules'),
                path.resolve(rootDir, 'node_modules'),
                path.resolve(rootDir, 'src/edu_modules/ke-common/dist/es6'),
            ],

        alias: 【对象】别名
        
        extensions: 【数组】导入时自动帮你补齐列表里的后缀名去找那个文件。
                 🌰例如 ['.jsx', '.js', '.ts', '.tsx'] 
        
        mainFields: 【字符串数组】引用第三方模块是引用他的哪个版本/优先级。
                 🌰例如 ['jsnext:main', 'browser', 'main']  表示ES6, 浏览器,es5
    }
    
    🌺plugins: // 插件数组
        🌰例如 new webpack.DefubePlugin({  创建全局变量
            a: xx
        })
        
    🌺devServer: { // webpack-dev-server配置
        hot: true 模块热替换。
        (DevServer 默认的行为是在发现源代码被更新后会通过自动刷新整个页面来做到实时预览,开启模块热替换功能后将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览。)
        
        host:ip
        
        port: 端口
        
        historyApiFallback: spa中如果刷新页面404,配置这个就可以指定404时跳去什么页面
    }
}

其他配置

{
    🌺target 构建出正对不同运行环境的代码,默认是web
    
    🌺devtool: source-map 开发调试的配置选项(🌰例如, 开发模式用cheap-module-eval-source-map, 既可以生成sourcemap耗时又最短,生产模式用hidden-source-map)
    
    🌺watch: true 监听文件更新,文件改变后重新编译。平常默认是关闭的,但是使用devServer时,默认开启。
    
    🌺externals: 不用重复再去打包的依赖.
            🌰例如,一个npm包引用的react包不应该打到包里),或者cdn引入的资源不需要打包
             {
               jquery: 'jQuery',
                react: 'React',
                'react-dom': 'ReactDOM',
                // 'react-dom/server': 'ReactDOMServer',
              },

    🌺optimization: //【对象】优化(webpack4之后不用自己配置,)
}

三、module的常用loader及使用

3.1 babel 把ES6转换为ES5、ES3 (babel-loader)

  • 最新ES 语法:比如,箭头函数
  • 最新ES API:,比如,Promise
  • 最新ES 实例方法:比如,String.protorype.includes

参考文章:

3.1.1 babel 6部分配置(.babelrc)

modules:默认使用 "commonjs"。即将代码中的ES6的import转为require。

  • @babel/preset-es2015:按照ES6标准编译
  • @babel-stage-x: 处理尚处在提案语法的插件集合,babel@7已经不推荐使用, stage-0的功能范围最大(通常使用建议配到@babel/preset-stage-2)
  • @babel-polyfill: api只能被polyfill
  • @babel-runtime + 插件@babel-plugin-transform-runtime配套使用。提取所有页面所需的helper函数到一个包里,避免重复注入(如打包后会变成require('babel-runtime/helpers/createClass'))

🌰事例:

{
  "sourceMaps": true,
  "presets": ["es2015","stage-2","react"], // 源码使用了哪些语法特性,需要提供支持.其实是一组plugins的集合
   "plugins": [ // 插件,控制如何转换代码
    [
      "transform-runtime", // transform-runtime 默认会自动的为你使用的 ES6 API 注入 polyfill, polyfill 的注入应该交给模块使用者,因为使用者可能在其它地方已经注入了其它的 Promise polyfill 库。所以关闭该功能
      {  "polyfill": false  } 
    ]
   ],
}

3.1.2 babel 7的部分配置(babel.config.js)

简单说,大部分babel 6的依赖命名从-改成了/

  • @babel/core 必需 根据我们的配置文件转换代码
  • @babel/present-env 相当于一个处理es6+规范语法的插件集合。 根据运行环境为代码做相应的编译, 包括了所有ECMA标准(bebal-preset-esxxxx),不包含state-x一些列插件,只支持最新推出版本的JavaScript语法(stage-4),如果需要支持其他stage需要额外引入。stage0表示最没定稿的新特性

image

  • @babel/runtime + @babel/plugin-transform-runtime 配套使用。 沙箱,提取所有页面所需的helper函数到一个包里,避免重复注入

🌰事例:

{
    // "plugins": ["@babel/plugin-transform-arrow-functions", "@babel/plugin-transform-destructuring"]    // 一个个插件不如使用preset-env就可以一次性全部引入
    // "presets": ["@babel/preset-env"]
    
    "presets": [
    "@babel/preset-env", 
    {
               "modules": false,
               "useBuiltIns": "entry", // 按需引入:在入口处把所有ie8以上浏览器不支持api的polyfill引入进来
                                       // 'usage',其功能更为强大,它会扫描你的代码,只有你的代码用到了哪个新的api,                        它才会引入相应的polyfill。【试验状态,谨慎使用】
               "targets": "ie >= 8" // 只有ie8以上版本浏览器不支持的语法才会被转换
    }
    '@babel/preset-react',
    '@babel/preset-typescript',
    ],
    "plugins": [
        "@babel/plugin-syntax-dynamic-import", //动态导入
        ["@babel/plugin-transform-runtime", {
            "corejs": 2
        }]                                // 所有的helper函数抽离到一个包中,由所有的文件共同引用则可以减少可观的代码量。也可以为你的代码创建一个sandboxed environment(沙箱环境),这在你编写一些类库等公共代码的时候尤其重要。
    ] 
}

3.2 TS转换成js (awesome-typescript-loader)

🌰tsconfig事例:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "baseUrl": ".",
    "paths": {
      "assets/*": ["src/assets/*"],
      "components/*": ["src/components/*"],
    },
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": true,
    "lib": ["es6", "es2017", "dom", "dom.iterable", "scripthost"],
    "target": "es5", // 编译出的代码使用ES几
    "module": "ESNext", // 编译出的代码采用的模块规范
    "jsx": "react",
    "noUnusedLocals": true,
    "typeRoots": ["node_modules/@types", "src/assets/types"],
    "sourceMap": true,
    "importHelpers": true, // 避免helper函数重复引入
    "types": ["jest", "node"]
  },
  "include": ["src/**/*"],
  "exclude": ["src/edu_modules/**/*", ".template/**/*"]
}

3.3 react解析

实现:

  • 方法1:结合babel,config里直接presets配置react
  • 方法2:结合ts,config里配置"compilerOptions": {
    "jsx": "react" // 开启 jsx ,支持 React
    }

但是需要入口文件改为.tsx,并且安装types/react @types/react-dom

🌰方法1事例:

{
  "presets": ["es2015", "stage-2", "react"],
  "plugins": [
    "react-hot-loader/babel",
    "transform-function-bind",
    "transform-class-properties",
    "transform-export-extensions",
    ],
    "env": {
      "backend": {
        "plugins": [
          [ "webpack-loaders",
            { "config": "./webpack.config.babel.js"
            , "verbose": true
            }
          ]
        ]
      }
    }
  }
}
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        user: {
          loader: 'babel-loader?cacheDirectory=true', // cacheDirectory用于缓存babel的编译结果,加快重新编译的速度
          options: {
            presets: ['@babel/preset-env'],
            plugins: [
              '@babel/plughin-transform-runtime',
              '@babel/plugin-transform-modules-commonjs'
            ]
          }
        }
      }
    ]
  }
}
  • 对于普通项目,可以直接使用preset-env配置polyfill
  • 对于类库项目,推荐使用@babel/runtime,需要注意一些实例方法的使用

3.4 样式相关的loader

loader的配置是从右往左的,且配置时可以省略“-loader”
(如 style-loader css-loader postcss-loader/sass-loader)

  • sass-loader // 解析sass为css
  • less-loader // 解析less为css
  • css-loader // 解析css。如@import,url()的导入语句 + 压缩css
  • style-loader, // 将解析后的样式嵌入js代码中。(将require引入的样式嵌入js文件中,有好处也有坏处。好处是减 少了请求数,坏处也很明显,就是当你的样式文件很大时,造成编译的js文件也很大。如果css文件较大时,可以可以用extract-text-webpack-plugin分开css文件和js,这样css和js并行加载加快加载速度)
  • postcss-loader + autoprefixer插件,可以为我们的样式添加前缀-webkit-,-ms- 提高兼容性

静态资源相关loader

  • url-loader 将图片转换为base64后注入到js和css中。(注意:会影响打包后文件大小,从而影响加载速度)
🌰事例:
{
        test: /\.png$/,
        use: [{
          loader: 'url-loader',
          options: {
            // 30KB 以下的文件采用 url-loader
            limit: 1024 * 30,
            // 否则采用 file-loader,默认值就是 file-loader 
            fallback: 'file-loader',
          }
        }]
      }
  • file-loader CSS 中导入图片的语句替换成正确的地址,并同时把文件输出到对应的位置。(比如css和js中把url相对路径的图片输出到dist文件夹,并取另一个带hash的名字)
🌰事例:
{
        test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf|svg|xlsx)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: '[path][name].[ext]',
            },
          },
        ],
      },
  • svg可以用图片用的file-loader和url-loader, 也可以用raw-loader ,svg-inline-loader。后面两种是应用于js引入时,输出是\<svg xxx>

参考文章:
https://github.com/zhengweike...

四、plugin插件

  • webpack-merge:提取公共配置,分离生产环境和开发环境的配置文件。clean-webpack-plugin:每次编译前自动清空dist目录
  • html-webpack-plugin:从html模板自动生成最终的html,它的AutoWebPlugin插件可以生成多页面应用
  • defineplugin:声明process.env.NODE_ENV的值
  • extract-text-webpack-plugin:从js提取css到css文件
  • optimize-css-assets-webpack-plugin:合并相同的css样式文件
  • css-split-webpack-plugin:拆分过大的csss文件
  • mini-css-extract-plugin:分开打包css文件
  • webpack-md5-hash:webpack自身生成文件hash值的方法是不确定的,为了确保hash值是根据文件内容生成的,可以使用该插件

五、resolve优化

  • resolve.extensions:使用require或import引入文件时可以省略后缀:
  • resolve.alias:简化引用路径:
  • resolve.mainFields:通过第三方插件的package.json中main字段(大多数第三方模块都采用这个字段)中的地址来引用文件
  • resolve.modules:限制引入第三方包的范围
  • resolve.mainFiles:在一定范围下默认优先查找的文件名

六、其他优化

  • optimization抽离公共组件代码
  • DllPlugin减少基础模块的编译次数
  • IgnorePlugin忽略某些模块打包
  • CopyWebpackPlugin生产环境中将提前构建的包同步到dist
  • uglifyjs去js代码压缩
  • web-webpack-plugin使用静态资源CDN
  • HappyPack进行多进程打包
  • webpack-bundle-analyzer打包结果进行分析

具体可以阅读《深入浅出webpack》

后记

因为webpack的知识点很多也很细,推荐阅读《深入浅出webpack》
这本书系统性,渐进性地讲了webpack的基础配置,项目实战,打包优化,插件及运行原理。如果想快速学习webpack并入门可以看看这本书,会比官网看起来更加有重点一些,但注意的是有些配置会有点过时了。
然后初学者可以多上手试试,这样印象更深刻也更能理解,我也在学习中~


psychola
66 声望2 粉丝

前端/copperplate