likes
comments
collection
share

前端进阶之Webpack(一)

作者站长头像
站长
· 阅读数 51

前言

不知不觉,工作两年了。

最近,参与搭建了一个微应用项目,由于使用的内部框架并不太成熟,而且是第一次参与构建大型项目,各种各样的坑,踩了个遍。

回想刚入行时,用vue-cli快速搭建项目,感觉搭项目也没啥难的,可谓是初生牛犊,不怕虎。

项目慢慢过渡到了迭代阶段,汇总一下热乎的知识,博客写了,就等于我没忘。

什么是Webpack?

在搭建项目的过程中,很多问题都可以总结成一个问题——兼容性问题。

剩下的问题,虽然五花八门,但始终围绕着“便捷”二字。

从作用上理解,Webpack是用来方便程序员开发,以及解决兼容性问题的工具。

从代码本质上理解,Webpack和其他依赖包没有区别,同样通过一个npm仓库进行管理,内部是nodejs语法。

不能少的node知识储备

node基础不错的朋友可以跳过这一节,但如果对node比较陌生的话,我建议还是看看,使用Webpack的过程中会用到。

模块化

一个依赖包就是一个模块,最基础的模块只需要两样东西:

  1. package.json:模块描述文件。
  2. [name].js:模块入口文件(模块的功能会被通过module.exports导出)。

前端进阶之Webpack(一)

前端进阶之Webpack(一) 上传模块的步骤:

  1. 注册仓库账号,没有公司仓库的话,可以直接去官网www.npmjs.com/注册一个。
  2. npm adduser:登录账号。
  3. npm publish:上传模块至对应的仓库。

前端进阶之Webpack(一)

上传之后,模块就和我们常见的依赖包一样可以被下载。

前端进阶之Webpack(一)

下载后,通过require引入模块就可以使用内部功能,我在demo中只是简单抛出了一个对象{a: '111'},我们常用的依赖包功能自然会复杂的多。

前端进阶之Webpack(一)

Webpack多了不少文件,但本质和我的demo是一样的,是node编写的包,都属于用户编写的模块,而httpfspath这些属于核心模块。

两者有什么区别?下面接着会讲。

模块引入

node中引入模块,会经历三个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

一个用户编写的模块,通过require('node_test_demo_sunshaohe')引入项目,没有加任何路径和文件后缀,node会先认为它是一个node自带的核心模块,找不到的话,会一层一层向外循环遍历寻找同名的.js文件,还是找不到的话,会去寻找同名的.json文件,最后寻找同名的.node文件。

因此,外部引入一个.node.json文件,最好写清楚路径和文件后缀。

核心模块在node源代码编译的过程中,就被编译成了二进制执行文件,而且在路径分析中会被优先判断,所以它的加载速度是最快的。

全局对象

由于自定义全局变量会造成环境污染,因此,需要尽可能利用node自带的全局对象。

__filename

__filename表示正在执行脚本的文件名(project/src/main.js),并附带绝对路径,如果在模块中,附带的是模块的路径(module/app.js)。

__dirname

__dirname表示执行脚本目录的绝对路径

process

在使用本地指令的时候,一般会用到process

由于我做的是多语言项目,某些国家会有专门的定制版本,在启动指令中用不同的COUNTRY值来区分,在项目中可以用process.env.COUNTRY获取环境变量。

process.env一次只能获取一个环境变量,在想要一次获取多个环境变量的时候,可以使用process.argv,它会以数组的形式,返回所有环境变量。

除此之外,process还有许多其他的属性和方法,感兴趣的朋友可以自行了解。

核心模块

path

path是最常用的核心模块,用于处理文件路径。

path.join():用于连接路径,正确使用相应系统的分隔符,Unix\Windows/

eg:path.join('a', 'b', 'c')等价于a/b/c

path.resolve():用于解析路径,从右往左分析,最终得到一个绝对路径。

eg:path.resolve('/a/b', './c/d'),解析后得到/a/b/c/d

eg:path.resolve('/a/b', '/c/d'),解析后得到/c/d

eg:path.resolve('/a/b', '../c/d'),解析后得到/a/c/d

Webpack的打包流程

  1. 读取Webpack的配置参数。
  2. 创建Compiler对象并开始解析项目。
  3. 从入口开始解析,找到其依赖的模块,递归遍历分析,形成依赖关系树。
  4. 对不同文件类型的模块,使用对应的loader进行编译,最终转换成JavaScript
  5. 根据入口和依赖关系,输出成果物。
  6. 整个打包过程会发布事件,触发订阅了对应事件的plugin

配置参数包括webpack.config.js里的固定配置和指令中的变量,Webpack会整合后,传给Compiler

以下是一个简易的Compliler类:

class Compiler(options) {
    constructor(options) {
        const {entry, output} = options; //options是配置参数
        this.entry = entry;
        this.output = output;
        this.modules = [];
    }
    
    run(){} //Webpack打包
    generate() {} //输出成果物
}

Webpack配置

Webpack默认会从webpack.config.js中读取配置参数,配置参数决定了Webpack会如何去打包,以及输出一个什么样的成果物。

context

基础目录context是一个绝对路径,Webpack解析相对路径时,会以context作为参考。

const path = require('path');
moduile.exports = {
    context: path.resolve(__dirname, 'app')
}

entry

Webpack从入口entry开始打包,entry支持三种数据类型:

  1. String:entry: './main.js'
  2. Array:entry: ['./main.js', './app.js']
  3. Object:entry: {a: './main.js', b: './app.js'}

output

output配置影响成果物如何输出。

filename

output.filename配置输出文件的名称,如果只有一个输出文件,可以写成:

module.exports = {
    output: {
        filename: 'bundle.js'
    }
}

有多个入口文件,需要输出多个文件时:

module.exports = {
    output: {
        filename: '[id][name][hash][chunkhash].js'
    }
}
  1. idChunk的唯一标识,从0开始。
  2. nameChuunk的名称,由入口文件的名称决定。
  3. hashChunk唯一标识的hash值。
  4. chunkhashChunk内容的hash值。

hashchunkhash的长度是可指定的,[hash: 8]代表取8位的hash值,默认是20位。

chunkFilename

output.chunkFilename配置无入口的Chunk在输出时的文件名称。

前端进阶之Webpack(一)

我用红框框住的就是无入口的Chunkapp.js则是入口文件的Chunk

Webpack默认会把异步加载的组件提取出来,脱离入口依赖图,单独打包成一个Chunk。我们还可以根据需要制定拆分规则,具体配置后续会讲。

path

output.path配置输出文件的目录,必须是绝对路径。

const path = require('path');
module.exports = {
    output: {
        path: 'path.resolve(__dirname, 'dist')'
    }
}

publicPath

output.publicPath配置线上资源的URL前缀。

const path = require('path');
module.exports = {
    output: {
        pubilicPath: 'http://example.com/assets/'
    }
}

在项目中可以通过http://example.com/assets/image.png获取资源。

library和libraryTarget

当用Webpack去构建一个可以被其他项目导入使用的库时,就会用到librarylibraryTarget

简而言之,在一个项目中引用另一个项目的成果物。

ouput.library配置导出库的名称。

output.libraryTarget配置以何种方式导出库,共有七种:

  1. umd:万能库,下面的每种库都有对应的使用方法,所有方法都可以用来调用umd库,不知道用什么的时候,用umd就对了。umd的兼容性自然是牺牲性能的,熟悉以后,做优化的时候,可以换成更合适的库。
  2. var:编写的库将通过var被赋值给通过library指定名称的变量。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'var'
    }
}

打包好的成果物,在另一个项目中使用:

// Webpack输出的代码
var LibraryName = lb_code;
// 使用库的方法
LibraryName.doSomething();
  1. commonjs:编写的库将通过CommonJS规范导出。node是按照CommonJS规范编写的。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'commonjs'
    }
}

打包好的成果物,在另一个项目中使用:

// Webpack输出的代码
exports['LibraryName'] = lib_code;
// 使用库的方法
require('library-name-in-npm')['LibraryName'].doSomething();
  1. commonjs2:编写的库将通过CommonJS2规范导出,与commonjs类似,在其基础上,可以通过module.exports的导出方式。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'commonjs2'
    }
}

打包好的成果物,在另一个项目中使用:

// Webpack输出的代码
module.exports = lib_code;
// 使用库的方法
require('library-name-in-npm')['LibraryName'].doSomething();
  1. this:编写的库将通过this被赋值给通过library指定的名称。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'this'
    }
}

打包好的成果物,在另一个项目中使用:

// Webpack输出的代码
this['LibraryName'] = lib_code;
// 使用库的方法
this.LibraryName.doSomething();
  1. window:编写的库将通过window被赋值给通过library指定的名称。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'window'
    }
}

打包好的成果物,在另一个项目中使用:

// Webpack输出的代码
window['LibraryName'] = lib_code;
// 使用库的方法
window.LibraryName.doSomething();
  1. global:编写的库将通过global被赋值给通过library指定的名称。
module.exports = {
    output: {
        library: 'libraryName',
        libraryTarget: 'global'
    }
}

打包好的成果物,在另一个项目中使用:

// Webpack输出的代码
global['LibraryName'] = lib_code;
// 使用库的方法
global.LibraryName.doSomething();

optimization

optimization用于对打包结果进行优化,之前提到的拆包规则便是在这里配置。

minimizer

optimization.minimizer是一个数组,会执行用于代码压缩的插件。

const TerserPlugin = require('teser-webpack-plugin');
module.exports = {
    optimization: {
        minimizer: [
            new TerserPlugin({
                // 开启多进程进行打包
                parallel: true;
                terserOptions: {
                    // 代码压缩
                    compress: {
                        // 打包时自动去掉console.log
                        drop_console: true,
                        // 打包时自动去掉debugger
                        drop_debugger: true
                    }
                }
            })
        ]
    }
}

splitChunks

optimization.splitChunks配置拆包规则。

module.exports = {
    optimization: {
        splitChunks: {
            // 规则的适用对象,all表示所有,async表示异步(默认),initial表示同步
            chunks: all,
            // Chunk大小超过30kb时,才进行代码拆分
            minSize: 30000,
            // 如果Chunk大小超过50kb,那么Webpack会尝试对其进行拆分
            maxSize: 50000,
            // 一个模块至少要被引用3次,才会被单独打包成一个Chunk
            minChunks: 3,
            // 如果Chunk的异步请求数超过5个,那么Webpack会尝试对其进行拆分
            maxAsyncRequests: 5,
            // 入口点能被拆分的最大数量
            maxInitialRequests: 1,
        }
    }
}

module

module配置处理模块的规则。

rules

module.rules是一个数组,读取Loader来解析对应类型的文件。

module.exports = {
    module: {
        rules: [
            {
                // 命中JavaScript文件
                test: /\.js$/,
                // 用babel-loader转换JavaScript文件
                use: ['babel-loader'],
                // 匹配范围是src下的文件
                include: path.resolve(__dirname, 'src'),
                // Loader的配置项
                options: {
                    preset: ['es2015']
                }
            },
            {
                test: /\.scss$/,
                // 使用一组Loader去处理scss文件
                // 会先执行sass-loader,再执行css-loader,最后执行style-loader
                // Webpack会按照配置从下到上,从右到左处理文件
                use: ['style-loader', 'css-loader', 'sass-loader'],
                // 搜索范围跳过node_modules下的文件
                exclude: path.resolve(__dirname, 'node_modules')
            }
        ]
    }
}

resolve

resolve配置Webpack如何寻找模块对应的文件。

modules

resolve.modules是一个数组,告诉Webpack去哪些目录下寻找第三方模块。之前提到过Webpack加载非核心模块的时候,需要写路径,但平时项目里引用的时候,并没有写路径,是因为这里做了统一处理。

module.exports = {
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')]
    }
}

alias

resolve.alias通过别名将原路径映射成一个新的路径。

module.exports = {
    resolve: {
        alias: {
            // 项目里绝对路径常用的@便是在这里配置
            '@': path.resolve(_dirname, 'src')
        }
    }
}

extensions

module.extensions是一个数组,用于拓展Webpack的文件支持项,之前说到过Webpack只能读取.js.json.node三种类型的文件,想要处理.ts.vue.jsx等类型的文件时,就需要做拓展。

module.exports = {
    resolve: {
        extensions: ['.ts', '.vue', '.jsx']
    }
}

devtool

devtool用于配置souce-map的精细程度,精细程度越高,打出来的包也越大。souce-map是打包后运行中的代码与本地代码之间的一个映射,改bug时,通过映射可以知道是本地哪块的代码出了问题。

module.exports = {
    // source-map/line-source-map/eval-source-map/hiden-source-map/cheap-source-map...
    // 下面是我们项目采用的配置,大家可以作为参考,放弃列,映射精确到行。
    devtool: 'cheap-module-eval-source-map'
}

devServer

只有安装了webpack-dev-server,通过devServer启服务时,配置才会生效。

proxy

proxy代理可以将浏览器到服务端的请求,转换成服务端与服务端之间的请求,绕过浏览器的同源性原则,从而避免跨域问题。

module.exports = {
    devServer: {
        proxy: 'http://localhost/demo:3000'
    }
}

总结

本来想写一篇实战总结的,结果快写完了,才发现似乎成了一篇知识归纳,干脆拆成两篇。

转载自:https://juejin.cn/post/7136895529354526756
评论
请登录