概念

  1. Webpack是一个打包模块化JavaScript的工具,它会从入口模块出发,识别出源码中的模块化导入语句,
  2. 递归的找出入口文件的所有依赖,将入口和其所有的依赖打包到一个单独的文件中。

安装

  • 推荐局部安装
  • 局部安装: 不推荐全局安装会造成版本指定,如果多个项目依赖的版本不同,会造成构建失败。
  1. yarn add webpack
  2. yarn add webpack-cli
  • 检查版本 npx webpack -v
  • 指定版本安装 yarn add webpack@xxx.xx
  • 启动 package.json "script": {"dev": "webpack"} yarn run dev

默认配置

  • 默认入口 : ./src/index.js
  • 默认输出 : ./dist 名称:main.js

    1. // webpack.config.js(默认命名)中的配置会覆盖webpack的默认配置
    2. const path = require('path');
    3. module.exports = {
    4. // webpack执行构建入口
    5. entry: "./src/index.js",
    6. output: {
    7. // 将所有依赖的模块合并输出到main.js
    8. filename: "main.js",
    9. // 输出文件的存放路径,必须是绝对路径
    10. // 根路径是config所在目录
    11. path: path.resolve(__dirname, "./dist")
    12. }
    13. };
  • 自定义webpack配置,package.json ---> "script": {"run name": "webpack --config webpack filename"}

  • webpack默认支持多种模块规范,如commonJS、ES module AMD
  • webpack默认支持js模块、json模块

核心概念

1. module

  1. // 主要对文件类型解析的loader配置
  2. // ...
  3. modules: {
  4. rules: [
  5. {test: /\...$/, use: ["file-loader"]},
  6. // loader具有执行顺序,自右往左
  7. {test: /\.css$/, use: ["style-loader", "css-loader", "postcss-loader"]}
  8. // ...
  9. ]
  10. }

拓展:css前缀自动填充
  1. // postcss.config.js
  2. module.exports = {
  3. plugins: [
  4. require('autoprefixer')({
  5. // 兼容最近两个版本, 市场占有率大于1%
  6. overrideBrowserslist: ['last 2 versions', '>1%']
  7. })
  8. ]
  9. }

2. plugins

  1. html-webpack-plugin

    1. // html-webpack-plugin
    2. const HtmlWebpackPlugin = require('html-webpack-plugin');
    3. //...
    4. plugins: [
    5. new HtmlWebpackPlugin({
    6. template: '', // 目标模板
    7. filename: '', // 编译后的模板名
    8. chunks: [] // 模板引入的主入口
    9. }),
    10. //...
    11. ]
  2. clean-webpack-plugin

    1. // clean-webpack-plugin 清除编译文件夹中冗余文件
    2. const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    3. //...
    4. plugins: [
    5. // 插件以config所在目录为根目录
    6. new CleanWebpackPlugin(['dist'], {
    7. // 指定config配置的上一级为根路径,此种配置为了解决类似vue将config集合到build文件夹情况
    8. root: path.resolve(__dirname, '../')
    9. }),
    10. //...
    11. ]
  3. mini-css-extract-plugin

    1. // mini-css-extract-plugin 整合css文件
    2. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    3. //...
    4. module: {
    5. rules: [
    6. {
    7. test: /\.css$/,
    8. // 使用MiniCssExtractPlugin的loader代替style-loader
    9. use: [MiniCssExtractPlugin.loader, 'css-loader', ...]
    10. }
    11. ]
    12. },
    13. plugins: [
    14. new MiniCssExtractPlugin({
    15. filename: '[name][hash:8].css' // 编译后css文件名
    16. }),
    17. ]

3. dev-tool

  1. // dev-tool 设置对应属性值,控制编译后是否生成map文件
  2. //...
  3. devtool:
  4. 'none', // 不生成,production环境
  5. 'source-map', // 生成map文件
  6. 'eval-source-map',
  7. //....

4. webpack-dev-server

  • 提升开发效率

    每次修改代码需要重新打包一次,打开浏览器然后刷新,安装使用webpackDevServer进行配置,可以解决以上问题。

  • 安装 npm install webpack-dev-server -D
  • 配置 ```javascript // package.json “scripts”: { “server”: “webpack-dev-server” }

// webpack.config.js devServer: { contentBase: ‘’, // 定位文件位置 open: true, // 自动打开浏览器 port: … // 端口号 proxy: { // 跨域代理 “/api”: { // 解析分割路径/api target: “http://localhost:8888“ } } }

  1. <a name="7fd0ef0f"></a>
  2. #### 5. `Hot Module Replacement(HMR: 热模块替换)`
  3. - `webpack`自带插件
  4. - ```javascript
  5. // webpack.config.js
  6. const webpack = require('webpack');
  7. devServer: {
  8. hotOnly: true, // 禁止刷新页面
  9. }
  10. // ...
  11. plugins: [
  12. new webpack.HotModuleReplacementPlugin(),
  13. ]
  • CSS样式

    HMR只支持cssstyle-loader处理方式,不支持抽离成独立文件的方式(mini-css-extract-plugin)

  • JS
  • JS模块,HMR 需要手动监听需要HMR的模块

    1. if(module.hot) {
    2. moudle.hot.accept('监听的模块', function(){...});
    3. }

Babel

BabelJavaScript编译器,能将ES6代码转换成ES5代码,让我们开发过程中放心使用JS新特性而不用担心兼容问题。并且还可以通过插件机制根据需求灵活的扩展。

Babel在执行编译的过程中,会从项目根目录下的.babelrc JSON文件中读取配置,没有该文件会从loaderoptions地方读取配置。

安装

npm i babel-loader @babel/core @babel/preset-env -D

ES6配置

babel-loaderwebpackbabel 的通信桥梁, @babel/preset-env 里包含了 ES6 7 8 转换为 ES5 的规则,类似Promise等语法仍需要特殊处理,使用@babel/polyfill babel —-> 分析依赖 —-> AST(抽象语法树) —-> 通过babel-preset中的规则转换 —-> 生成代码

  1. // webpack.config.js
  2. {
  3. test: /\.js$/,
  4. exclude: /node_modiles/,
  5. loader: 'babel-loader',
  6. options: {
  7. presets: [['@babel/preset-env', {
  8. targets: {
  9. edge: '17',
  10. firefox: '60',
  11. chrome: '67',
  12. safari: '11.1'
  13. }, // 浏览器支持版本
  14. corejs: 2, // 新版本需要指定核心库版本
  15. useBuiltIns: 'usage' // 按需注入
  16. }]]
  17. }
  18. }
  19. // useBuiltIns是babel7的新功能,这个选项告诉babel如何配置@babel/polyfill
  20. // 参数: entry usage false
  21. // entry: 需要在webpack的入口文件里import "@babel/polyfill"一次,babel会根据你的使用情况导入垫片,没有使用的功能不会被导入相应的垫片。
  22. // usage: 不需要import, 全自动检测, 但是要安装@babel/polyfill。
  23. // false: 如果import @babel/polyfill, 整个polyfill库引入项目中,导致打包体积很大。

React配置

安装@babel/preset-react

  1. {
  2. test: /\.js$/,
  3. exclude: /node_modiles/,
  4. loader: 'babel-loader',
  5. options: {
  6. presets: [
  7. ['@babel/preset-env'],
  8. '@babel/preset-react'
  9. ]
  10. }
  11. }

单页面(SPA)多入口打包

  1. // webpack.config.js
  2. {
  3. entry: {
  4. index: './src/index.js',
  5. list: './src/list.js',
  6. other: './src/other.js'
  7. },
  8. output: {
  9. filename: '[name].js',
  10. path: path.resolve(__dirname, './build')
  11. },
  12. //...
  13. plugins: [
  14. new htmlWebpackPlugin({
  15. template: './src/index.html',
  16. filename: 'index.html',
  17. chunks: ['index'] // 打包编译后,html引入对应名的js文件
  18. }),
  19. new htmlWebpackPlugin({
  20. template: './src/list.html',
  21. filename: 'list.html',
  22. chunks: ['list']
  23. }),
  24. new htmlWebpackPlugin({
  25. template: './src/other.html',
  26. filename: 'other.html',
  27. chunks: ['other']
  28. }),
  29. ]
  30. }

MPA打包

  1. const glob = require('glob');
  2. const setMap = () => {
  3. const entry = {};
  4. const htmlwebpackplugin = [];
  5. //! 分析入口文件路径
  6. const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));
  7. entryFiles.map((item, index) => {
  8. const entryFile = entryFiles[index];
  9. //! 过滤信息拿到入口名称
  10. const match = entryFile.match(/src\/(.*)\/index\.js/);
  11. const pageName = match && match[1];
  12. entry[pageName] = entryFile;
  13. //! 配置htmlWebpackPlugin
  14. htmlwebpackplugin.push(
  15. new htmlWebpackPlugin({
  16. template: 'src/index.html',
  17. filename: `${pageName}.html`,
  18. chunks: [pageName]
  19. })
  20. )
  21. })
  22. return {
  23. entry,
  24. htmlwebpackplugin
  25. }
  26. }
  27. const { entry, htmlwebpackplugin } = setMap();

自定义实现webpack打包构建功能

webpack.js

  1. /**
  2. * * 获取配置,根据配置信息启动webpack,执行构建
  3. * * 1. 从入口模块开始分析 //? 1.1 有哪些依赖 1.2 转换文件
  4. * * 2. 递归的分析其他依赖模块 //? 2.1 有哪些依赖 2.2 转换文件
  5. * * 3. 生成可以在浏览器端执行的bundle文件
  6. **/
  7. //! 启动webpack node webpack.js
  8. // webpack.config.js 会返回一个对象,包含各种配置
  9. const options = require('./webpack.config.js');
  10. const Compiler = require('./lib/compile.js');
  11. new Compiler(options).run();

lib/compiler.js

  1. const { getAst, getDependencies, getCode } = require('./parser.js');
  2. const path = require('path');
  3. const fs = require('fs');
  4. class Compiler {
  5. constructor(options){
  6. this.entry = options.entry;
  7. this.output = options.output;
  8. this.modules = [];
  9. }
  10. run() {
  11. const info = this.build(this.entry);
  12. this.modules.push(info);
  13. for (let i = 0; i < this.modules.length; i++) {
  14. const { dependencies } = this.modules[i];
  15. if(dependencies) {
  16. for (let item in dependencies) {
  17. this.modules.push(this.build(dependencies[item]));
  18. }
  19. }
  20. }
  21. // 转换数据结构
  22. let obj = {};
  23. this.modules.forEach(item => {
  24. obj[item.fileName] = {
  25. dependencies: item.dependencies,
  26. code: item.code
  27. }
  28. });
  29. console.log(obj);
  30. // 生成代码文件
  31. this.file(obj);
  32. }
  33. build(fileName) {
  34. // 1. 分析入口,读取入口模块的内容
  35. // 2. 读取的内容通过babel/parser解析成AST抽象语法树
  36. let ast = getAst(fileName);
  37. // 3. 使用babel/traverse对AST进行解析,获取入口文件依赖的相对路径
  38. let dependencies = getDependencies(ast, fileName);
  39. // 4. 使用babel/core中的transformFromAst对AST进行解析转换
  40. let code = getCode(ast);
  41. return {
  42. fileName,
  43. dependencies,
  44. code
  45. }
  46. }
  47. file(code) {
  48. // 获取输出信息
  49. const filePath = path.join(this.output.path, this.output.fileName);
  50. const newCode = JSON.stringify(code);
  51. /**
  52. {
  53. './src/index.js': {
  54. dependencies: {
  55. './Hello.js': 'src\\Hello.js'
  56. },
  57. code: '"use strict";\n\nvar _Hello = require("./Hello.js");\n\nconsole.log(\'hello\' + (0, _Hello.say)(\'你好\'));'
  58. },
  59. 'src\\Hello.js': {
  60. dependencies: {},
  61. code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.say= say;\n\nfunction say(key) {\n return key;\n}'
  62. }
  63. }
  64. */
  65. // 传入解析后的代码文件
  66. // 根据入口路径进行代码引入并执行
  67. const bundle = `(function(graph){
  68. function require(module) {
  69. function localRequire(relativePath) {
  70. return require(graph[module].dependencies[relativePath]);
  71. }
  72. var exports = {};
  73. // 执行code时,如果遇到require,会递归调用localRequire,返回引入的文件
  74. (function(require, exports, code){
  75. eval(code);
  76. })(localRequire, exports, graph[module].code);
  77. return exports;
  78. }
  79. // 入口路径
  80. require('${this.entry}')
  81. })(${newCode})`;
  82. fs.writeFileSync(filePath, bundle, 'utf-8');
  83. }
  84. }
  85. module.exports = Compiler;

lib/parser.js

  1. const parser = require('@babel/parser');
  2. const traverse = require('@babel/traverse').default;
  3. const {transformFromAst} = require('@babel/core');
  4. const fs = require('fs');
  5. const path = require('path');
  6. module.exports = {
  7. // 分析模块 获取ast抽象语法树
  8. getAst: fileName => {
  9. let content = fs.readFileSync(fileName, 'utf-8');
  10. return parser.parse(content, {
  11. sourceType: 'module'
  12. })
  13. },
  14. // 获取依赖项路径对象集
  15. getDependencies: (ast, fileName) => {
  16. // ! 可以保留相对路径和绝对路径
  17. let dependencies = {};
  18. traverse(ast, {
  19. ImportDeclaration({node}) {
  20. const dirname = path.dirname(fileName);
  21. const newPath = path.join(dirname, node.source.value);
  22. dependencies[node.source.value] = newPath;
  23. }
  24. });
  25. return dependencies;
  26. },
  27. // 获取ast解析后转换的文件信息输出
  28. getCode: ast => {
  29. const { code } = transformFromAst(ast, null, {
  30. presets: [ require('@babel/preset-env') ]
  31. });
  32. return code; // '"use strict";\n\nvar _Hello = require("./Hello.js");\n\nconsole.log(\'hello\' + (0, _Hello.say)(\'你好\'));'
  33. },
  34. }

自定义Loader

  1. // webpack.config.js
  2. //...省略
  3. module: {
  4. rules: [
  5. {
  6. test: ...,
  7. loader: path.resolve(__dirname, '...'), // 绝对路径
  8. options: {...} // 在自定义loader中可以使用query获取到options内容,也可以使用loader-utils工具集
  9. }
  10. ]
  11. }
  12. // 自定义loader
  13. module.exports = function selfLoader(source) {
  14. source // 调用该loader时,传入的资源文件
  15. this // node对象
  16. return ... // 必须有返回值
  17. }

loader多个返回值

  1. // 自定义loader
  2. module.exports = function selfLoader(source) {
  3. // 返回多个信息,可以使用this.callback
  4. // this.callback(
  5. // err: Error | null,
  6. // content: string | Buffer,
  7. // sourceMap?: SourceMap,
  8. // meta?: any
  9. // );
  10. return this.callback(null, str);
  11. }

loader的异步操作

  1. // loader中有异步操作,使用this.async声明为异步操作 结果值使用callback进行返回。
  2. const callback = this.async();
  3. // ....
  4. return callback(null, str);

多个loader

  1. // 自定义loader可以进行配置,从而只需写loader文件名也可被查找到
  2. resolveLoader: ['node_modules', './loaders'];
  3. // 多个loader引入,如果有先后关系,按照从右向左顺序引入

自定义Plugin

基本结构

  1. class CopyrightWebpackPlugin {
  2. constructor(){}
  3. // compiler: webpack实例
  4. apply(compiler){}
  5. }
  6. module.exports = CopyrightWebpackPlugin;

compiler的hooks函数

  1. class CopyrightWebpackPlugin {
  2. constructor(options){}
  3. // compiler webpack实例
  4. apply(compiler){
  5. // 在输出到目标文件夹之前进行异步写入,同步是tap,异步是tapAsync
  6. compiler.hooks.emit.tapAsync(
  7. "CopyrightWebpackPlugin",
  8. (compilation, cb) => {
  9. compilation.assets["deankwan.txt"] = {
  10. // 文件内容
  11. source: function() {
  12. return "dean kwan";
  13. },
  14. // 文件大小
  15. size: function() {
  16. return 20;
  17. }
  18. }
  19. cb();
  20. }
  21. )
  22. }
  23. }
  24. module.exports = CopyrightWebpackPlugin;