美文网首页
Webpack实践过程

Webpack实践过程

作者: 王国的荣耀 | 来源:发表于2020-08-08 09:56 被阅读0次

    Webpack 中文文档
    https://www.webpackjs.com/concepts/

    webpack 4个核心概念:

    1. 入口(entry): webpack所有依赖关系图的起点。
    2. 出口(output): 指定webpack打包应用程序的地方。
    3. 加载器(loader): 指定加载的需要处理的各类文件。
    4. 插件(plugins): 定义项目要用到的插件。
    image.png

    webpack与gulp区别

    1. gulp是工具链、构建工具。可以配合各种插件做jps.压缩,css.压缩,less编译等,可以替代手工实现自动化工作。
    2. 而webpack是文件打包工具,可以把项目的各种jps.文、css.文件等打包合并成一个或多个文件,主要用于模块化方案,预编译模块的方案。
    3. 在定义和使用类比中两者都有各的用途,同时webpack为初级编译程序,gulp为高级编译程序,在功能上要比webpack应用程序中多。
    4. webpack可以很方便使用node_module、es6或者样式注入等功能,作为最初级的功能定位性价比最高,webpack输入输出都以js为主,对html兼顾较少,可用组件不多很难达到可用的程度。
    5. gulp在编程方面较为复杂,但是可用的组件也会更多,手动编译的情况下耗时较长,同时此软件不适合初级入门者使用。

    webpack 的优势

    1. Webpack默认支持多种模块标准,包括AMD、CommonJS,以及最新的ES6模块,而其他工具大多只能支持一到两种。这对于一些同时使用多种模块标准的工程非常有用,Webpack会帮我们处理好不同类型模块之间的依赖关系。
    2. Webpack有完备的代码分割(code splitting)解决方案。从字面意思去理解,它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态地加载。这对于资源体积较大的应用来说尤为重要,可以有效地减小资源体积,提升首页渲染速度。
    3. Webpack可以处理各种类型的资源。除了JavaScript以外,Webpack还可以处理样式、模板,甚至图片等,而开发者需要做的仅仅是导入它们。比如你可以从JavaScript文件导入一个CSS或者PNG,而这一切最终都可以由第4章讲到的loader来处理。

    webpack实践过程

    命令行编译

    mkdir hellodemo
    cd hellodemo
    npm init -y
    
    touch index.js
    touch content.js
    touch index.html
    
    //安装了webpack以及webpack-cli。webpack是核心模块,webpack-cli则是命令行工具
    npm install webpack webpack-cli 
    
    // 安装结束之后,在命令行执行npx webpack -v以及npx webpack-cli -v,可显示各自的版本号,即证明安装成功。
    ➜  hellodemo npx webpack -v
    4.44.1
    ➜  hellodemo npx webpack-cli -v
    3.3.12
    
    

    js 文件

    //index.js
    import addContent from "./content.js";
    document.write("My first Webpack app.<br />");
    addContent();
    
    //content.js
    export default function () {
      document.write("Hello world!");
    }
    
    //index.html
    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
        <meta charset="UTF-8">
        <title>My first Webpack app.</title>
    </head>
    
    <body>
        <script src="./dist/bundle.js"></script>
    </body>
    </html>
    
     //webpack 
    ➜  hellodemo npx webpack --entry=./index.js --output-filename=bundle.js --mode=development
    Hash: a5c12b72b48858006303
    Version: webpack 4.44.1
    Time: 63ms
    Built at: 2020-08-07 4:00:41 ├F10: PM┤
        Asset      Size  Chunks             Chunk Names
    bundle.js  4.51 KiB    null  [emitted]  null
    Entrypoint null = bundle.js
    [./content.js] 65 bytes {null} [built]
    [./index.js] 101 bytes {null} [built]
    
    //命令行的第1个参数entry是资源打包的入口。Webpack从这里开始进行模块依赖的查找,得到项目中包含index.js和content.js两个模块,并通过它们来生成最终产物。
    //命令行的第2个参数output-filename是输出资源名。你会发现打包完成后工程中出现了一个dist目录,其中包含的bundle.js就是Webpack的打包结果。
    //最后的参数mode指的是打包模式。Webpack为开发者提供了development、production、none三种模式。
    
    

    脚本编译

    //修改package.json
    // Webpack默认的源代码入口就是src/index.js,可以省略掉entry的配置。
     "scripts": {
        "build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development"
      },
    
    // content.js 修改为
    export default function() {
        document.write('I\'m using npm scripts!');
    }
    
    // vs code 选择运行build 或者 npm run build
    

    webpack-dev-server 编译

    webpack-dev-server的两大职能

    1. 令Webpack进行模块打包,并处理打包结果的资源请求。
    2. 作为普通的Web Server,处理静态资源文件请求。

    直接用Webpack开发和使用webpack-dev-server有一个很大的区别,前者每次都会生成budnle.js,而webpack-dev-server只是将打包结果放在内存中,并不会写入实际的bundle.js,在每次webpack-dev-server接收到请求时都只是将内存中的打包结果返回给浏览器。

    //  1. 安装
    npm install webpack-dev-server
    
    // 2. content.js 修改为
    export default function() {
        document.write('I\'m using webpack-dev-server!');
    }
    
    // 3. 将index.js和content.js移动到src目录下
    
    //4. 新建webpack.config.js文件
    module.exports = {
        entry: './src/index.js'
        output: {
            filename: './bundle.js',
        },
        mode: 'develpoment',
        devServer: {
            publicPath: '/dist',
        },
    };
    
    //5. 修改 package.json
    "dev": "webpack-dev-server"
    
    //6.  npm run dev
    
    

    模块打包

    commonjs

    exports

    // 第一种方式
    exports.name = 'calculater';
    exports.add = function(a, b) {
        return a + b;
    };
    
    // 第二种方式
    module.exports = {
        name: 'calculater',
        add: function(a, b) {
            return a + b;
        }
    };
    
    错误的exports
    //在使用exports时要注意一个问题,即不要直接给exports赋值,否则会导致其失效。如:
    exports = {
        name: 'calculater'
    };
    

    上面代码中,由于对exports进行了赋值操作,使其指向了新的对象,module.exports却仍然是原来的空对象,因此name属性并不会被导出。

    另一个在导出时容易犯的错误是不恰当地把module.exports与exports混用。

    exports.add = function(a, b) {
        return a + b;
    };
    module.exports = {
        name: 'calculater'
    };
    

    上面的代码先通过exports导出了add属性,然后将module.exports重新赋值为另外一个对象。这会导致原本拥有add属性的对象丢失了,最后导出的只有name。

    总结:为了提高可读性,建议将module.exports及exports语句放在模块的末尾。

    require

    当我们require一个模块时会有两种情况:
    ·require的模块是第一次被加载。这时会首先执行该模块,然后导出内容。
    ·require的模块曾被加载过。这时该模块的代码不会再次执行,而是直接导出上次执行后得到的结果。

    ES6 module

    模块

    // calculator.js
    export default {
        name: 'calculator',
        add: function(a, b) {
            return a + b;
        }
    };
    
    // index.js
    import calculator from './calculator.js';
    const sum = calculator.add(2, 3);
    console.log(sum); // 5
    

    ES6 export

    // 写法1
    export const name = 'calculator';
    export const add = function(a, b) { return a + b; };
    
    // 写法2
    const name = 'calculator';
    const add = function(a, b) { return a + b; };
    export { name, add };
    

    第1种写法是将变量的声明和导出写在一行;第2种则是先进行变量声明,然后再用同一个export语句导出。两种写法的效果是一样的。

    命名导出

    在使用命名导出时,可以通过as关键字对变量重命名。如:

    const name = 'calculator';
    const add = function(a, b) { return a + b; };
    export { name, add as getSum }; // 在导入时即为 name 和 getSum
    

    默认导出

    与命名导出不同,模块的默认导出只能有一个。如:

    export default {
        name: 'calculator',
        add: function(a, b) {
            return a + b;
        }
    };
    

    ES6 import

    // calculator.js
    const name = 'calculator';
    const add = function(a, b) { return a + b; };
    export { name, add };
    
    // index.js
    import { name, add } from './calculator.js';
    add(2, 3);
    

    CommonJS与ES6 Module 区别

    动态与静态

    CommonJS与ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,而后者是“静态的”。在这里“动态”的含义是,模块依赖关系的建立发生在代码运行阶段;而“静态”则是模块依赖关系的建立发生在代码编译阶段。

    CommonJS 脚本

    // calculator.js
    module.exports = { name: 'calculator' };
    
    // index.js
    const name = require('./calculator.js').name;
    

    在CommonJS模块被执行前,并没有办法确定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。

    ES6 Module 脚本

    // calculator.js
    export const name = 'calculator';
    
    // index.js
    import { name } from './calculator.js';
    

    ES6 Module的导入、导出语句都是声明式的,它不支持导入的路径是一个表达式,并且导入、导出语句必须位于模块的顶层作用域(比如不能放在if语句中)。因此我们说,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就可以分析出模块的依赖关系。它相比于CommonJS来说具备以下几点优势:

    在ES6代码的编译阶段就可以分析出模块的依赖关系。它相比于CommonJS来说具备以下几点优势:

    • 死代码检测和排除。我们可以用静态分析工具检测出哪些模块没有被调用过。比如,在引入工具类库时,工程中往往只用到了其中一部分组件或接口,但有可能会将其代码完整地加载进来。未被调用到的模块代码永远不会被执行,也就成为了死代码。通过静态分析可以在打包时去掉这些未曾使用过的模块,以减小打包资源体积。
    • 模块变量类型检查。JavaScript属于动态类型语言,不会在代码执行前检查类型错误(比如对一个字符串类型的值进行函数调用)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接口类型是正确的。
    • 编译器优化。在CommonJS等动态模块系统中,无论采用哪种方式,本质上导入的都是一个对象,而ES6 Module支持直接导入变量,减少了引用层级,程序效率更高。

    值拷贝与动态映射

    在导入一个模块时,对于CommonJS来说获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,并且这个映射是只读的。

    CommonJS 导入

    //cal.js
    var count = 0;
    module.exports = {
      count: count,
      add: function (a, b) {
        count += 1;
        // console.log(count);
        return a + b;
      },
    };
    
    // index.js
    var count = require('./cal.js').count;
    var add = require('./cal.js').add;
    
    console.log(count); // 0(这里的count是对 calculator.js 中 count 值的拷贝)
    console.log(add(2, 3));
    console.log(count); // 0(calculator.js中变量值的改变不会对这里的拷贝值造成影响)
    
    var newData = require("./cal.js").count;
    console.log(newData); // 0(calculator.js中变量值的改变不会对这里的拷贝值造成影响)
    
    count += 1;
    console.log(count); // 1(拷贝的值可以更改)
    console.log(require("./cal.js").count); 
    

    ES6导入

    // cal.js
    let count = 0;
    const add = function (a, b) {
      count += 1;
      return a + b;
    };
    export { count, add };
    
    import { count, add } from "./cal.js" ;
    
    console.log(count); // 0(对 calculator.js 中 count 值的映射)
    add(2, 3);
    console.log(count); // 1(实时反映calculator.js 中 count值的变化)
    
    

    总结:ES6 Module导入的变量进行更改,可以将这种映射关系理解为一面镜子,从镜子里我们可以实时观察到原有的事物,但是并不可以操纵镜子中的影像。

    循环依赖

    ES6 Module的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。

    模块打包过程

    // index.js
    const calculator = require('./calculator.js');
    const sum = calculator.add(2, 3);
    console.log('sum', sum);
    
    // calculator.js
    module.exports = {
        add: function(a, b) {
            return a + b;
        }
    };
    

    打包之后的bundle.js

    // 立即执行匿名函数
    (function(modules) {
        //模块缓存
        var installedModules = {};
        // 实现require
        function __webpack_require__(moduleId) {
            ...
        }
        // 执行入口模块的加载
        return __webpack_require__(__webpack_require__.s = 0);
    })({
        // modules:以key-value的形式储存所有被打包的模块
        0: function(module, exports, __webpack_require__) {
            // 打包入口
            module.exports = __webpack_require__("3qiv");
        },
        "3qiv": function(module, exports, __webpack_require__) {
            // index.js内容
        },
        jkzz: function(module, exports) {
            // calculator.js 内容
        }
    });
    

    bundle.js 分析

    • 最外层立即执行匿名函数。它用来包裹整个bundle,并构成自身的作用域。
    • installedModules对象。每个模块只在第一次被加载的时候执行,之后其导出值就被存储到这个对象里面,当再次被加载的时候直接从这里取值,而不会重新执行。
    • webpack_require函数。对模块加载的实现,在浏览器中可以通过调用webpack_require(module_id)来完成模块导入。
    • modules对象。工程中所有产生了依赖关系的模块都会以key-value的形式放在这里。key可以理解为一个模块的id,由数字或者一个很短的hash字符串构成;value则是由一个匿名函数包裹的模块实体,匿名函数的参数则赋予了每个模块导出和导入的能力。

    bundle如何运行

    一个bundle是如何在浏览器中执行的。
    1)在最外层的匿名函数中会初始化浏览器执行环境,包括定义installedModules对象、webpack_require函数等,为模块的加载和执行做一些准备工作。

    2)加载入口模块。每个bundle都有且只有一个入口模块,在上面的示例中,index.js是入口模块,在浏览器中会从它开始执行。

    3)执行模块代码。如果执行到了module.exports则记录下模块的导出值;如果中间遇到require函数(准确地说是webpack_require),则会暂时交出执行权,进入webpack_require函数体内进行加载其他模块的逻辑。

    4)在webpack_require中会判断即将加载的模块是否存在于installedModules中。如果存在则直接取值,否则回到第3步,执行该模块的代码来获取导出值。

    5)所有依赖的模块都已执行完毕,最后执行权又回到入口模块。当入口模块的代码执行到结尾,也就意味着整个bundle运行结束。

    webpack 资源输入输出

    配置资源入口

    Webpack通过context和entry这两个配置项来共同决定入口文件的路径。在配置入口时,实际上做了两件事:

    • 确定入口模块位置,告诉Webpack从哪里开始进行打包。
    • 定义chunk name。如果工程只有一个入口,那么默认其chunk name为“main”;如果工程有多个入口,我们需要为每个入口定义chunk name,来作为该chunk的唯一标识。

    context

    context可以理解为资源入口的路径前缀,在配置时要求必须使用绝对路径的形式。

    // 以下两种配置达到的效果相同,入口都为 <工程根路径>/src/scripts/index.js
    module.exports = {
        context: path.join(__dirname, './src'),
        entry: './scripts/index.js',
    };
    module.exports = {
        context: path.join(__dirname, './src/scripts'),
        entry: './index.js',
    };
    

    entry

    与context只能为字符串不同,entry的配置可以有多种形式:字符串、数组、对象、函数。

    配置资源出口

    与出口相关的配置都集中在output对象里。

    const path = require('path');
    module.exports = {
        entry: './src/app.js',
        output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'assets'),
            publicPath: '/dist/',
        },
    };
    

    filename的作用是控制输出资源的文件名,其形式为字符串。
    path可以指定资源输出的位置,要求值必须为绝对路径。
    publicPath是一个非常重要的配置项,并且容易与path相混淆。从功能上来说,path用来指定资源的输出位置,而publicPath则用来指定资源的请求位置。让我们详细解释这两个定义。

    webpack-dev-server的配置中也有一个publicPath,值得注意的是,这个publicPath与Webpack中的配置项含义不同,它的作用是指定webpack-dev-server的静态资源服务路径。

    const path = require('path');
    module.exports = {
        entry: './src/app.js',
        output: {
            filename: 'bundle.js',
            path: path.join(__dirname, 'dist'),
        },
        devServer: {
            publicPath: '/assets/',
            port: 3000,
        },
    };
    

    Loader加载器

    每个loader本质上都是一个函数。
    用公式表达loader的本质则为以下形式:
    output=loader(input)
    这里的input可能是工程源文件的字符串,也可能是上一个loader转化后的结果,包括转化后的结果(也是字符串类型)、source map,以及AST对象;output同样包含这几种信息,转化后的文件字符串、source map,以及AST。如果这是最后一个loader,结果将直接被送到Webpack进行后续处理,否则将作为下一个loader的输入向后传递。

    插件(plugins)

    loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

    plugins用于接收一个插件数组,我们可以使用Webpack内部提供的一些插件,也可以加载外部插件。

    HtmlWebpackPlugin

    https://www.webpackjs.com/plugins/html-webpack-plugin/
    HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。 你可以让插件为你生成一个HTML文件,使用lodash模板提供你自己的模板,或使用你自己的loader

    其它

    webpack devtool

    https://webpack.js.org/configuration/devtool/

    此选项控制是否以及如何生成源映射。
    使用SourceMapDevToolPlugin进行更细粒度的配置。请参阅源映射加载器来处理现有的源映射。

    Choose a style of source mapping to enhance the debugging process. These values can affect build and rebuild speed dramatically.
    选择源映射的样式以增强调试过程。这些值会显著影响构建和重建速度。

    webpack resolve

    https://webpack.js.org/configuration/resolve/

    These options change how modules are resolved. webpack provides reasonable defaults, but it is possible to change the resolving in detail. Have a look at Module Resolution for more explanation of how the resolver works.
    这些选项会更改模块的解析方式。webpack提供了合理的默认值,但可以详细更改解析。查看模块解析,了解解析器如何工作的更多说明。

    webpack SourceMap

    SourceMap是一种映射关系。当项目运行后,如果出现错误,错误信息只能定位到打包后文件中错误的位置。如果想查看在源文件中错误的位置,则需要使用映射关系,找到对应的位置。

    // 会生成map格式的文件,里面包含映射关系的代码
    devtool: 'source-map',
    
    //不会生成map格式的文件,包含映射关系的代码会放在打包后生成的代码中
    devtool: 'inline-source-map',
    
    // cheap有两种作用:一是将错误只定位到行,不定位到列。二是映射业务代码,不映射loader和第三方库等。会提升打包构建的速度。
    devtool: 'inline-cheap-source-map',
    
    // module会映射loader和第三方库
    devtool: 'inline-cheap-module-source-map',
    
    用eval的方式生成映射关系代码,效率和性能最佳。但是当代码复杂时,提示信息可能不精确。
    devtool: 'eval',
    
    // 推荐
    // 开发环境
    devtool: 'cheap-module-eval-source-map',
    // 生产环境
    devtool: 'cheap-module-source-map',
    

    Node.js

    Node.js并非一个JavaScript框架,而是一个集成了Google V8 JavaScript引擎、事件驱动和底层I/O API,并且可使用JavaScript语言开发服务器端应用的运行环境。与PHP不同的是,Node.js可以直接提供网络服务,不需要借助Apache、Nginx等专业的服务器软件。

    webpack config sample

    webpack.dev.config.js

    const path = require('path');
    console.log(path);
    
    const webpack = require('webpack');
    const WebpackMerge = require("webpack-merge");
    const baseConfig = require('./webpack.base.js'); // 引用公共配置
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    
    const devConfig = {
      mode: "development", // 开发模式
      devtool: "cheap-module-eval-source-map",
      entry: path.join(__dirname, "../example/src/app.js"), // 项目入口,处理资源文件的依赖关系
      output: {
        path: path.join(__dirname, "../example/src/"),
        filename: "bundle.js", // 使用 webpack-dev-sevrer 启动开发服务时,并不会实际在`src`目录下生成bundle.js,打包好的文件是在内存中的,但并不影响我们使用。
      },
      resolve: {
        // 一定不要忘记配置ts tsx后缀
        extensions: [".tsx", ".ts", ".js"],
        alias: {
          "@utils": path.resolve("./src/utils"), // 这样配置后 utils 可以指向 src/utils 目录
          "@images": path.resolve("./src/images"), // 这样配置后 utils 可以指向 src/images 目录
          "@components": path.resolve("./src/components"), // 这样配置后 utils 可以指向 src/images 目录
        },
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            exclude: /\.min\.css$/,
            loader: ["style-loader", "css-loader", "typed-css-modules-loader"],
          },
          {
            test: /\.min\.css$/,
            loader: ["style-loader", "css-loader"],
          },
          {
            test: /\.less$/,
            exclude: [
              /node_modules/,
              path.resolve(__dirname, "../src/components/photoGallery/fonts"),
            ],
            use: [
              "style-loader",
              {
                loader: "@teamsupercell/typings-for-css-modules-loader",
                options: {
                  formatter: "prettier",
                },
              },
              {
                loader: "css-loader",
                options: {
                  modules: {
                    localIdentName: "[local]_[hash:base64:5]",
                  },
                  sourceMap: true,
                  importLoaders: 2,
                  // camelCase: true,
                  // localsConvention: "camelCase",
                },
              },
              // 'typed-css-modules-loader',
              "less-loader",
            ],
          },
          {
            test: /\.less$/,
            include: path.resolve(
              __dirname,
              "../src/components/photoGallery/fonts"
            ),
            use: ["style-loader", "css-loader", "less-loader"],
          },
          {
            test: /\.(eot|woff2?|woff|ttf|svg|otf)$/,
            use: ["file-loader"],
          },
          {
            test: /\.(png|jpg|gif)$/,
            use: {
              loader: "url-loader",
              options: {
                limit: 8192,
                name: "[name]_[hash:7].[ext]",
                outputPath: "images/",
                esModule: false, // 出现图片 src 是 module 对象
              },
            },
          },
        ],
      },
      plugins: [
        new HtmlWebpackPlugin({
          template: path.join(__dirname, "../src/index.html"),
          filename: "index.html",
        }),
        new webpack.WatchIgnorePlugin([/css\.d\.ts$/]),
      ],
      devServer: {
        contentBase: path.join(__dirname, "../example/src/"),
        compress: true,
        port: 3002, // 启动端口为 3001 的服务
        open: true, // 自动打开浏览器
      },
    };
    module.exports = WebpackMerge.merge(devConfig, baseConfig); // 将baseConfig和devConfig合并为一个配置
    
    

    参考书籍

    https://www.webpackjs.com/concepts/
    Webpack实战:入门、进阶与调优
    深入浅出Webpack

    相关文章

      网友评论

          本文标题:Webpack实践过程

          本文链接:https://www.haomeiwen.com/subject/abwhrktx.html