Webpack 是一个现代 JavaScript 应用程序的静态模块打包器 (module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图 (dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

Webpack思想:一切皆模块

所有项目中使用到的依赖文件都被视为模块,webpack 做的就是把这些模块进行处理,进行一系列的 转换、压缩、合成、混淆 操作,把项目文件打包成最原始的静态资源。

简单体验

创建目录结构

mkdir webpack-demo cd webpack-demo npm init -y mkdir dist mkdir src

在 dist 目录下创建 index.html

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <script src="main.js"></script> <body> </body> </html>

在 src 目录下创建index.js

function timeout(time) { return new Promise(resolve => { setTimeout(resolve, time) }) } async function asyncPrint(value, time) { await timeout(time) console.log(value) } asyncPrint('hello,world', 5000)

安装webpack

在这之前你要安装好 nodejs,并保证她是 最新的版本 然后,进行以下步骤以保证你能位于中国pc 机上顺利的安装 npm 包。

  1. 设定中国镜像

npm install -g mirror-config-china --registry=http://registry.npm.taobao.org # 检查是否安装成功 npm config list

  1. 安装 Windows build 环境(npm 上的有些包是 native 的,需要编译)

npm install -g windows-build-tools

//全局安装 npm install webpack webpack-cli -g // **推荐** 局部安装,写入开发环境依赖 npm install webpack webpack-cli -D

执行webpack

# 以下代码生效基于webpack4零配置,只用于演示 # 如果为局部安装 npx webpack # 使用 npx 命令,相当于他会在当前目录的 node_modules 的 .bin 目录下执行命令 # 此处未加 mode 选项,应该会有 warning,默认 mode 为production # 增加mode后 npx webpack --mode development

webpack 会默认 src/index.js 文件为入口,dist/main.js 为出口打包

❯ npx webpack Hash: ce7e1fea469219fad208 // 本次打包对应唯一一个hash值 Version: webpack 4.42.1 // 本次打包对应webpack版本 Time: 69ms // 消耗的时间 Built at: 2020-03-27 11:25:08 // 构建的时刻 Asset Size Chunks Chunk Names // 打包后的文件名,大小,分包的 id,入口文件名 main.js 1.02 KiB 0 [emitted] main Entrypoint main = main.js [0] ./src/index.js 245 bytes {0} [built]

正式开始

webpack4 拥有一些默认配置但是他并不是以零配置作为宣传口号,如果喜欢零配置使用的话,可以去看 parceljs,或者 ncc pkg(nodejs),而且对于不同的项目,我们往往需要高度的可定制性,这时候就需要我们自己写配置文件。

在项目目录下创建 webpack.config.js (默认配置文件地址), 可以通过 --config <文件> 显式指定

//常用配置模块 module.exports = { entry: '', // 入口文件 output: {}, // 出口文件 devtool: '', // 错误映射 (source-map) mode: '', // 模式配置 (production / development ) module: {}, // 处理对应模块 (对文件做些处理) plugins: [], // 插件 (压缩/做其他事情的) devServer: {}, // 开发服务器配置 optimization: {}, // 压缩和模块分离 resolve: {}, // 模块如何解析,路径别名 }

入口 (entry) 与出口 (output)

以下就是 webpack 的默认配置

const path = require('path'); module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'main.js' } };

写好配置文件后再次

npx webpack

效果不变

借用 npm script,在 package.json 中 scripts 属性增加 script 如下

{ ... "scripts": { "dev": "webpack --mode development -w", "build": "webpack --mode production" } ... }

模式(mode)

module.exports = { ... mode: 'development' || 'production' ... }

mode 写入配置文件后,执行 webpack 时就不用再带 mode 选项

理解这两种模式容易,关键是根据不同的模式对 webpack 做不同的配置,因为不同模式下我们对代码的需求不一样。

开发项目时,通常会写两套不同的配置,一套用于开发环境,一套用于生产环境,两套不同配置包括三个配置文件,分别为

webpack如何学习(webpack基本入门)(1)

以基础配置文件为入口,根据环境变量判断当前环境,使用 webpack-merge 插件融合相应环境配置文件。

npm install -D webpack-merge

//webpack.config.js const path = require('path') const merge = require('webpack-merge') const devConfig = require('./webpack.dev.js') const prodConfig = require('./webpack.prod.js') const commonConfig = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), }, } module.exports = env => { if (env && env.production) { return merge(commonConfig, prodConfig) } else { return merge(commonConfig, devConfig) } } // webpack.dev.js module.exports = { mode: 'development', output: { filename: '[name].js', }, }; // webpack.prod.js module.exports = { mode: 'production', output: { filename: '[name].[contenthash].js', }, }

对scripts字段改写如下

因为我需要经常修改配置文件, 我们需要监控文件修改,然后重启 webpack,所以需要先安装 nodemon

npm install -D nodemon

{ "scripts": { "dev": "nodemon --watch webpack.*.js --exec \"webpack -w\"", "build": "webpack --env.production" } }

devtool(错误映射)(source-map)

devtool构建速度重新构建速度生产环境品质(quality)(none) yes打包后的代码eval no生成后的代码cheap-eval-source-map no转换过的代码(仅限行)cheap-module-eval-source-mapo no原始源代码(仅限行)eval-source-map-- no原始源代码cheap-source-map oyes转换过的代码(仅限行)cheap-module-source-mapo-yes原始源代码(仅限行)inline-cheap-source-map ono转换过的代码(仅限行)inline-cheap-module-source-mapo-no原始源代码(仅限行)source-map----yes原始源代码inline-source-map----no原始源代码hidden-source-map----yes原始源代码nosources-source-map----yes无源代码内容

在 webpack.dev.js 和 webpack.prod.js 中分别加入 source-map

//webpack.dev.js module.exports = { mode: 'development', devtool: 'source-map', output: { filename: '[name].js', } } //webpack.prod.js module.exports = { mode: 'production', devtool: 'cheap-module-source-map', output: { filename: '[name].[contenthash].js' } }

plugins(插件)

插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项 (option) 自定义, 有些插件你也可以在 一个单独配置文件 中进行配置。

需要通过使用 new 操作符来创建它的一个实例。

在我看来,plugins的主要作用有:

两个简单插件示例

npm install html-webpack-plugin -D

... const HtmlWebpackPlugin = require('html-webpack-plugin'); ... const commonConfig = { ... plugins: [ new HtmlWepackPlugin({ template: 'public/index.html', }), ] ... }

npm install clean-webpack-plugin -D

const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { mode: 'production', ... plugins: [new CleanWebpackPlugin()], }

module (loader)(文件预处理)

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!(官方)

在我看来,loader 的主要作用有:

下面会引入 css、图片依赖,为使目录结构清晰,分别打包进单独文件夹

加载CSS

npm install css-loader style-loader mini-css-extract-plugin -D

在配置文件中加入 loader

注意:

//webpack.dev.js module.exports = { mode: 'development', devtool: 'cheap-module-eval-source-map', output: { filename: 'js/[name].js', }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, } // webpack.prod.js ... const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { mode: 'production', ... output: { filename: 'js/[name].[contenthash].js', // 这里修改成 js 文件夹下面 }, module: { rules: [ { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../', }, // 文件打包至dist/css目录下,需配置 publicPath,以防等会引入图片出错 }, 'css-loader', ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].[hash:8].css', // css 样式打包到 css 文件夹下面 }), ... ], }

处理 less

npm install less-loader less -D

// webpack.prod.js ... module.exports = { ... module: { rules: [ { test: /\.less$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../', }, // 文件打包至dist/css目录下,需配置publicPath,以防等会引入图片出错 }, 'css-loader', 'less-loader', ] } ], }, ... } // webpack.dev.js module.exports = { ... module: { rules: [ { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'], } ], }, }

打包图片

在CSS等文件引入图片

npm install file-loader url-loader -D

在 webpack.config.js 中

... const commonConfig = { ... module: { rules: [ { test: /\.(jpe?g|png|gif|svg)$/, use: [ { loader: 'url-loader', options: { limit: 8192, outputPath: 'images/', }, }, ], }, ], }, .... } ...

url-loader 配合 file-loader,在 options 中限制添加 limit 可以把指定大小图片编码成 base64,减少网络请求。

babel —— 转义ES6 代码

babel 默认只转换语法, 而不转换新的API, 如需使用新的API, 还需要使用对应的转换插件,例如,默认情况下babel可以将箭头函数,class 等语法转换为 ES5 兼容的形式,但是却不能转换 Map,Set,Promise等新的全局对象,这时候就需要使用 polyfill 去模拟这些新特性。

# babel 核心 npm install babel-loader @babel/core -D # @babel/plugin-transform-runtime 这个会创建一些辅助函数,以防污染全局 # @babel/plugin-transform-regenerator async 转换 # @babel/runtime-corejs3 corejs 是一个 polyfill npm install @babel/plugin-transform-runtime @babel/runtime-corejs3 @babel/plugin-transform-regenerator -D

在 webpack.config.js 中

... const commonConfig = { ... module: { rules: [ { test: /\.js$/, exclude: '/node_modules/', use: 'babel-loader', }, ... }, ... } ...

在项目目录下新建 .babelrc.json 文件,写入options

这里只是为了演示方便而写的配置,并不符合实际,一般网页项目都和结合 @babel/preset-env 和 .browserslistrc 来使用,如果想看他们的区别请看 https://segmentfault.com/a/1190000021188054 写的比较清楚。

// .babelrc.json { "plugins": [ [ "@babel/plugin-transform-runtime", { "corejs": 3, "proposals": true, "useESModules": true } ], ["@babel/plugin-transform-regenerator"] ] }

devServer

npm install webpack-dev-server -D

每次编写完代码后,都要重新 npm run dev,为了提高开发效率,我们借助 webpack-dev-server 配置本地开发服务,主要字段如下:

// webpack.config.js { ... devServer: { contentBase: './dist', //配置开发服务运行时的文件根目录 port: 3000, //端口 hot: true, //是否启用热更新 open: false, //是否自动打开浏览器 }, ... }

借助devServer,我们可以

"scripts": { "dev": "nodemon --watch webpack.*.js --exec \"webpack-dev-server\"", "build": "webpack --env.production" },

// webpack.dev.js const webpack = require('webpack'); ... plugins: [ new webpack.HotModuleReplacementPlugin() ] ...

// ./src/test.js export default () => { console.log(1) } // ./src/index.js import test from './test'; ... if (module.hot) { module.hot.accept('./test.js', function() { test(); }) } test(); ...

到这里就大概给大家介绍了一下 webpack 应该覆盖大部分内容,当然不可能这么快就熟悉,我觉得比较高效的学习方法就是 https://webpack.docschina.org/guides/installation

看 webpack 官方的 guides (写的非常仔细)走一遍应该就能比较好的理解了。

原理

我觉得实现一个简单的 bundler 是了解原理比较好的方法吧,所以这里就带大家来看怎么自己实现一个类似于 webpack 的打包工具,不要听着觉得很复杂,其实比较简单,因为这次没有做比较基础的一些工作了,直接调了别人的库。

// ./bundler.js // nodejs 文件处理 const fs = require('fs'); // nodejs 文件路径 const path = require('path'); // 生成 ast 的库 const parser = require('@babel/parser'); // 遍历 ast const traver = require('@babel/traverse').default; // babel es6 -> es5 const babel = require('@babel/core'); /** * 生成转义后的代码以及依赖关系 * @param filePath * @returns {{code: string, filePath: string, dependencies: {}}} */ const moduleAnalyser = filePath => { // 拿到文件内容 const content = fs.readFileSync(filePath, 'utf-8'); // 生成 ast const ast = parser.parse(content, { // 使用 es module sourceType: 'module', }); // 建立一个对象来接遍历的依赖 // key: relativePath -> 也就是 import xx from '<relativePath>' // value: 唯一的绝对路径 const dependencies = {}; // 遍历 ast traver(ast, { ImportDeclaration ({ node }) { const relativePath = node.source.value; dependencies[relativePath] = path.join(path.dirname(filePath) relativePath.slice(1)); } }); // 转义之后的代码 const { code } = babel.transformSync(content, { presets: ['@babel/preset-env'] }); return { filePath, dependencies, code, }; }; /** * 生成依赖关系图 * @param entry * @returns {{ * dependencies: {} * code: string * }} */ const makeDependenciesGraph = entry => { // 入口模块 const entryModule = moduleAnalyser(entry); // 关系图 const graphArray = [entryModule]; for (let i = 0; i < graphArray.length; i ) { // 拿到本次的 dependencies const { dependencies } = graphArray[i]; if (dependencies) { // 遍历 dependencies,推到关系图中 for (const j in dependencies) { graphArray.push(moduleAnalyser(dependencies[j])); } } } const graph = {}; // 转换结构成对象 // key: 绝对路径 // value: dependencies, code graphArray.forEach(item => { graph[item.filePath] = { dependencies: item.dependencies, code: item.code }; }); return graph; }; /** * 生成代码 * @param entry * @returns {string} */ const generatorCode = entry => { const graph = makeDependenciesGraph(entry); console.log(graph); return ` (function(graph){ function require(module) { function localRequire(relativePath){ return require(graph[module].dependencies[relativePath]) } var exports = {}; (function(require, code, exports){ eval(code) })(localRequire, graph[module].code, exports); return exports; }; require(${JSON.stringify(entry)}); })(${JSON.stringify(graph)}); ` }; console.log(generatorCode(path.resolve(__dirname, 'src', 'index.js')));

// src/index.js import message from './message.js'; console.log(message); // src/message.js import { word } from './word.js'; const message = `hello ${word}`; export default message; // src/word.js export const word = 'world';

参考 https://juejin.im/post/5cb0948ce51d456e6154b3f4

最后再说一下 babel 到这里也没有结束,比如说 Babel handbook Babel Macros 之类的学习资源,而且他们都能做出能够帮助到你帮助到其他人的东西,而且为了解了原理之后为 webpack 编写 loader 和 plugins 都比较简单的。

,