范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

前端构建工具进化历程

  作者:buzhanhua序言
  现在前端项目的开发过程离不开构建工具帮助,面对琳琅满目的构建工具我们该如何选择最合适自己场景的构建工具是一个问题。在研究各种配置之余,我们去研究一下构建工具发展过程、底层原理,面对一些问题的时候往往事半功倍。
  通过本文你可以了解到: 前端构建工具的进化历程 前端构建工具技术方案对比 常用构建工具核心实现原理 什么是构建?
  构建简单的说就是将我们开发环境的代码,转化成生产环境可用来部署的代码。
  市面上存在很多构建工具,但是最终的目标都是转化开发环境的代码为生产环境中可用的代码。在不同的前端项目中使用的技术栈是不一样的,比如:不同的框架、不同的样式处理方案等,为了生产出生产环境可用的 JS、CSS,构建工具实现了诸如:代码转换、代码压缩、tree shaking、code spliting 等。 前端构建工具可以做什么?
  前端构建工具进化史
  无模块化时代
  YUI Tool + Ant
  YUI Tool 是 07 年左右出现的一个构建工具,可以实现压缩混淆前端代码,依赖于  java  的 ant  使用。
  在早期 web 应用开发主要采用  JSP ,处于混合开发的状态,不像是现在的前后端分离开发。通常由 java 开发人员来编写 js、css 代码。此时出现的构建工具依赖于别的语言实现。
  JS 内联外联
  前端代码是否必须通过构建才可以在浏览器中运行呢?当然不是。如下:         Hello World                    
  上述代码,我们只需要按格式写几个 HTML 标签,插入简单的 JS 脚本,打开浏览器,一个 Hello World 的前端页面就呈现在我们面前了。但是当项目进入真正的实战开发,代码规模开始急速扩张后,大量逻辑混杂在一个文件之中就变得难以维护起来。早期的前端项目一般如下组织:         JQuery                         
  通过 JS 的内联外联组织代码,将不同的代码放在不同的文件中,但是这也仅仅解决了 代码组织混乱 的问题,还存在很多问题:大量的全局变量,代码之间的依赖是不透明的,任何代码都可能悄悄的改变了全局变量。 脚本的引入需要依赖特定的顺序。
  后续出现过一些  IIFE、命名空间 等解决方案,但是从本质上都没有解决依赖全局变量通信的问题。在这样的背景下,前端工程化成为解决此类问题的正轨。
  社区模块化时代
  AMD/CMD - 异步模块加载
  为了解决浏览器端 JS 模块化的问题,出现了通过引入相关工具库的方式来解决这一问题。出现了两种应用比较广的规范及其相关库: AMD(RequireJs) 和 CMD(Sea.js)。AMD 推崇依赖前置、提前执行 ,CMD 推崇依赖就近、延迟执行 。下面领略下相关写法:
  Require.js // 加载完jquery后,将执行结果 $ 作为参数传入了回调函数 define(["jquery"], function ($) {     $(document).ready(function(){         $("#root")[0].innerText = "Hello World";     })     return $ })
  Sea.js // 预加载jquery define(function(require, exports, module) {     // 执行jquery模块,并得到结果赋值给 $     var $ = require("jquery");     // 调用jquery.js模块提供的方法     $("#header").hide(); });
  两种模块化规范实现的原理基本上是一致的,只不过各自坚持的理念不同。两者都是以异步的方式获取当前模块所需的模块,不同的地方在于 AMD 在获取到相关模块后立即执行,CMD 则是在用到相关模块的位置再执行的。
  AMD/CMD 解决问题: 手动维护代码引用顺序。从此不再需要手动调整 HTML 文件中的脚本顺序,依赖数组会自动侦测模块间的依赖关系,并自动化的插入页面。 全局变量污染问题。将模块内容在函数内实现,利用闭包导出的变量通信,不会存在全局变量污染的问题。
  Grunt/Gulp
  在 Google Chrome 推出 V8 引擎后,基于其高性能和平台独立的特性,Nodejs 这个 JS 运行时也现世了。至此,JS 打破了浏览器的限制,拥有了文件读写的能力。Nodejs 不仅在服务器领域占据一席之地,也将前端工程化带进了正轨。
  在这个背景下,第一批基于 Node.js 的构建工具出现了。
  Grunt
  Grunt[1]  主要能够帮助我们自动化的处理一些反复重复的任务,例如压缩、编译、单元测试、linting 等。 // Gruntfile.js module.exports = function(grunt) {    // 功能配置   grunt.initConfig({     // 定义任务     jshint: {       files: ["Gruntfile.js", "src/**/*.js", "test/**/*.js"],       options: {         globals: {           jQuery: true         }       }     },     // 时时侦听文件变化所执行的任务     watch: {       files: ["<%= jshint.files %>"],       tasks: ["jshint"]     }   });   // 加载任务所需要的插件   grunt.loadNpmTasks("grunt-contrib-jshint");   grunt.loadNpmTasks("grunt-contrib-watch");   // 默认执行的任务   grunt.registerTask("default", ["jshint"]);  };
  Gulp
  Grunt 的 I/O 操作比较"呆板",每个任务执行结束后都会将文件写入磁盘,下个任务执行时再将文件从磁盘中读出,这样的操作会产生一些问题: 运行速度较慢 硬件压力大
  Gulp[2]  最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递。同时 Gulp 设计简单,既可以单独使用,也可以结合别的工具一起使用。// gulpfile.js const { src, dest } = require("gulp"); // gulp提供的一系列api // src 读取文件 // dest 写入文件 const babel = require("gulp-babel");  exports.default = function() {   // 将src文件夹下的所有js文件取出,经过Babel转换后放入output文件夹之中   return src("src/*.js")     .pipe(babel())     .pipe(dest("output/")); }
  Browserify
  随着 Node.js 的兴起, CommonJS 模块化规范 成为了当时的主流规范。但是我们知道 CommonJS 所使用的 require 语法是同步的,当代码执行到 require 方法的时候,必须要等这个模块加载完后,才会执行后面的代码。这种方式在服务端是可行的,这是因为服务器只需要从本地磁盘中读取文件,速度还是很快的,但是在浏览器端,我们通过网络请求获取文件,网络环境以及文件大小都可能使页面无响应。
  browserify[3]  致力于打包产出在浏览器端可以运行的 CommonJS 规范的 JS 代码。var browserify = require("browserify") var b = browserify() var fs = require("fs")  // 添加入口文件 b.add("./src/browserifyIndex.js") // 打包所有模块至一个文件之中并输出bundle b.bundle().pipe(fs.createWriteStream("./output/bundle.js"))
  browserify 怎么实现的呢?
  browserify 在运行时会通过进行 AST 语法树分析,确定各个模块之间的依赖关系,生成一个依赖字典。之后包装每个模块,传入依赖字典以及自己实现的 export 和 require 函数,最终生成一个可以在浏览器环境中执行的 JS 文件。
  browserify 专注于 JS 打包,功能单一,一般配合 Gulp 一起使用。
  ESM 规范出现
  在 2015 年 JavaScript 官方的模块化终于出现了,但是官方只阐述如何实现该规范,浏览器少有支持。
  Webpack
  其实在 ESM 标准出现之前,  webpack[4] 已经诞生了,只是没有火起来。webpack 的理念更偏向于工程化,伴随着 MVC 框架以及 ESM 的出现与兴起,webpack2 顺势发布,宣布支持 AMDCommonJSESM、css/less/sass/stylus、babel、TypeScript、JSX、Angular 2 组件和 vue 组件。从来没有一个如此大而全的工具支持如此多的功能,几乎能够解决目前所有构建相关的问题。至此 webpack 真正成为了前端工程化的核心。
  webpack 是基于配置。 module.exports = {     // SPA入口文件     entry: "src/js/index.js",     // 出口     output: {       filename: "bundle.js"     }     // 模块匹配和处理 大部分都是做编译处理     module: {         rules: [                     // babel转换语法             { test: /.js$/, use: "babel-loader" },             //...         ]     },     // 插件     plugins: [         // 根据模版创建html文件         new HtmlWebpackPlugin({ template: "./src/index.html" }),     ], }
  webpack 要兼顾各种方案的支持,也暴露出其缺点: 配置往往非常繁琐,开发人员心智负担大。 webpack 为了支持 cjs 和 esm,自己做了 polyfill,导致产物代码很"丑"。
  在 webpack 出现两年后,rollup 诞生了~
  Rollup
  rollup[5]  是一款面向未来的构建工具,完全基于 ESM 模块规范进行打包,率先提出了 Tree-Shaking 的概念。并且配置简单,易于上手,成为了目前最流行的 JS 库打包工具。import resolve from "rollup-plugin-node-resolve"; import babel from "rollup-plugin-babel";  export default {   // 入口文件   input: "src/main.js",   output: {     file: "bundle.js",     // 输出模块规范     format: "esm"   },   plugins: [     // 转换commonjs模块为ESM     resolve(),     // babel转换语法     babel({       exclude: "node_modules/**"     })   ] }
  rollup 基于 esm,实现了强大的 Tree-Shaking 功能,使得构建产物足够的简洁、体积足够的小。但是要考虑浏览器的兼容性问题的话,往往需要配合额外的 polyfill 库,或者结合 webpack 使用。
  ESM 规范原生支持
  Esbuild
  在实际开发过程中,随着项目规模逐渐庞大,前端工程的启动和打包的时间也不断上升,一些工程动辄几分钟甚至十几分钟,漫长的等待,真的让人绝望。这使得打包工具的性能被越来越多的人关注。
  esbuild[6] 是一个非常新的模块打包工具,它提供了类似 webpack 资源打包的能力,但是拥有着超高的性能。
  esbuild 支持 ES6/CommonJS 规范、Tree Shaking、TypeScript、JSX 等功能特性。提供了 JS API/Go API/CLI 多种调用方式。 // JS API调用 require("esbuild").build({     entryPoints: ["app.jsx"],     bundle: true,     outfile: "out.js",  }).catch(() => process.exit(1))
  根据官方提供的性能对比,我们可以看到性能足有百倍的提升,为什么会这么快?
  语言优势 esBuild 是选择 Go 语言编写的,而在 esBuild 之前,前端构建工具都是基于 Node,使用 JS 进行编写。JavaScript 是一门解释性脚本语言,即使 V8 引擎做了大量优化(JWT 及时编译),本质上还是无法打破性能的瓶颈。而 Go 是一种编译型语言,在编译阶段就已经将源码转译为机器码,启动时只需要直接执行这些机器码即可。 Go 天生具有多线程运行能力,而 JavaScript 本质上是一门单线程语言。esBuild 经过精心的设计,将代码 parse、代码生成等过程实现完全并行处理。
  性能至上原则 esBuild 只提供现代 Web 应用最小的功能集合,所以其架构复杂度相对较小,更容易将性能做到极致 在 webpack、rollup 这类工具中, 我们习惯于使用多种第三方工作来增强工程能力。比如:babel、eslint、less 等。在代码经过多个工具流转的过程中,存在着很多性能上的浪费,比如:多次进行 代码 -> AST、AST -> 代码 的转换。esBuild 对此类工具完全进行了定制化重写,舍弃部分可维护性,追求极致的编译性能。
  虽然 esBuild 性能非常高,但是其提供的功能很基础,不适合直接用到生产环境,更适合作为底层的模块构建工具,在它基础上进行二次封装。
  Vite
  vite[7]  是下一代前端开发与构建工具,提供 noBundle 的开发服务,并内置丰富的功能,无需复杂配置。
  vite 在开发环境和生产环境分别做了不同的处理,在开发环境中底层基于 esBuild 进行提速,在生产环境中使用 rollup 进行打包。
  为什么 vite 开发服务这么快?
  传统 bundle based 服务: 无论是 webpack 还是 rollup 提供给开发者使用的服务,都是基于构建结果的。 基于构建结果提供服务,意味着提供服务前一定要构建结束,随着项目膨胀,等待时间也会逐渐变长。
  noBundle 服务: 对于 vite、snowpack 这类工具,提供的都是 noBundle 服务,无需等待构建,直接提供服务。 对于项目中的第三方依赖,仅在初次启动和依赖变化时重构建,会执行一个 依赖预构建  的过程。由于是基于 esBuild 做的构建,所以非常快。对于项目代码,则会依赖于浏览器的 ESM 的支持,直接按需访问,不必全量构建。
  为什么在生产环境中构建使用 rollup? 由于浏览器的兼容性问题以及实际网络中使用 ESM 可能会造成 RTT 时间过长,所以仍然需要打包构建。 esbuild 虽然快,但是它还没有发布 1.0 稳定版本,另外 esbuild 对代码分割和 css 处理等支持较弱,所以生产环境仍然使用 rollup。
  目前 vite 发布了 3.0 版本,相对于 2.0,修复了 400+issue,已经比较稳定,可以用于生产了。Vite 决定每年发布一个新的版本。
  vite.config.js: import { defineConfig } from "vite" import vue from "@vitejs/plugin-vue" import {resolve} from "path"; // defineConfig 这个方法没有什么实际的含义, 主要是可以提供语法提示 export default defineConfig({   resolve:{     alias:{      "@":resolve("src")     }   },   plugins: [vue()] }) 技术方案对比
  前端构建工具实在是琳琅满目,以工程化的视角对社区仍然比较流行的构建工具进行简单对比,一些功能专一、特定场景下的工具不在考虑范围内~。
  2021 前端构建工具排行[8]
  一些值得思考的问题
  为什么 webpack 构建产物看着很丑?
  我们在使用 webpack 构建项目后,会发现打包出来的代码非常的"丑",这是为什么?原因就是:webpack 支持多种模块规范,但是最后都会变成 commonJS规范  (webpack5 对纯 esm 做了一定的优化),但是浏览器不支持commonJS规范  ,于是 webpack 自己实现了require  和module.exports  ,所以会有很多 polyfill 代码的注入。
  针对·common.js 加载 common.js 这种情况分析一下构建产物。
  源代码: // src/index.js let title = require("./title.js") console.log(title);  // src/title.js module.exports = "bu";
  产物代码:  (() => {     //把所有模块定义全部存放到modules对象里     //属性名是模块的ID,也就是相对于根目录的相对路径,包括文件扩展名     //值是此模块的定义函数,函数体就是原来的模块内的代码    var modules = ({      "./src/title.js": ((module) => {        module.exports = "bu";      })    });    // 缓存对象    var cache = {};     // webpack 打包后的代码能够运行, 是因为webpack根据commonJS规范实现了一个require方法    function require(moduleId) {      var cachedModule = cache[moduleId];      if (cachedModule !== undefined) {        return cachedModule.exports;      }      // 缓存和创建模块对象      var module = cache[moduleId] = {        exports: {}      };      // 运行模块代码      modules[moduleId](module, module.exports, require "moduleId");      return module.exports;    }    var exports = {};    (() => {      // 入口相关的代码      let title = require("./src/title.js")      console.log(title);    })();  })();
  webpack 按需加载的模块怎么在浏览器中运行?
  在实际项目开发中,随着代码越写越多,构建后的 bundle 文件也会越来越大,我们往往按照种种策略对代码进行按需加载,将某部分代码在用户事件触发后再进行加载,那么 webpack 在运行时是怎么实现的呢?
  其实原理很简单,就是以 JSONP 的方式加载按需的脚本,但是如何将这些异步模块使用起来就比较有意思了~
  对一个简单的 case 进行分析。
  源代码: // index.js import("./hello").then((result) => {     console.log(result.default); });  // hello.js export default "hello";
  产物代码:
  main.js  // PS: 对代码做了部分简化及优化, 否则太难读了~~~  // 定一个模块对象 var modules = ({}); // webpack在浏览器里实现require方法 function require(moduleId) {xxx}  /**  * chunkIds 代码块的ID数组  * moreModules 代码块的模块定义 */ function webpackJsonpCallback([chunkIds, moreModules]) {   const result = [];   for(let i = 0 ; i < chunkIds.length ; i++){     const chunkId = chunkIds[i];     result.push(installedChunks[chunkId][0]);     installedChunks[chunkId] = 0; // 表示此代码块已经下载完毕   }    // 将代码块合并到 modules 对象中去   for(const moduleId in moreModules){     modules[moduleId] = moreModules[moduleId];   }   //依次将require.e方法中的promise变为成功态   while(result.length){     result.shift()();   } }  // 用来存放代码块的加载状态, key是代码块的名字 // 每次打包至少产生main的代码块 // 0 表示已经加载就绪 var installedChunks = {   "main": 0 }  require.d = (exports, definition) => {   for (var key in definition) {     Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });   } }; require.r = (exports) => {   Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });   Object.defineProperty(exports, "__esModule", { value: true }); };  // 给require方法定义一个m属性, 指向模块定义对象 require.m = modules;  require.f = {};  // 利用JSONP加载一个按需引入的模块 require.l = function (url) {   let script = document.createElement("script");   script.src = url;   document.head.appendChild(script); }  // 用于通过JSONP异步加载一个chunkId对应的代码块文件, 其实就是hello.main.js require.f.j = function(chunkId, promises){   let installedChunkData;   // 当前代码块的数据   const promise = new Promise((resolve, reject) => {     installedChunkData = installedChunks[chunkId] = [resolve, reject];   });   promises.push(installedChunkData[2] = promise);   // 获取模块的访问路径   const url = chunkId + ".main.js";    require.l(url); }  require.e = function(chunkId) {   let promises = [];   require.f.j(chunkId, promises);   console.log(promises);   return Promise.all(promises); }  var chunkLoadingGlobal = window["webpack"] = []; // 由于按需加载的模块, 会在加载成功后调用此模块,所以这是JSONP的成功后的回掉 chunkLoadingGlobal.push = webpackJsonpCallback;  /**  * require.e异步加载hello代码块文件 hello.main.js  * promise成功后会把 hello.main.js里面的代码定义合并到require.m对象上,也就是modules上  * 调用require方法加载./src/hello.js模块,获取 模块的导出对象,进行打印  */ require.e("hello").then(require.bind(require, "./src/hello.js")).then(result => console.log(result));
  hello.main.js "use strict"; (self["webpack"] = self["webpack"] || []).push([   ["hello"], {     "./src/hello.js": ((module, exports, require) => {       require.r(exports);       require.d(exports, {         "default": () => (_DEFAULT_EXPORT__)       });       const _DEFAULT_EXPORT__ = ("hello");     })   } ]);
  webpack 在产物代码中声明了一个全局变量  webpack   并赋值为一个数组,然后改写了这个数组的 push 方法。在异步代码加载完成后执行时,会调用这个 push 方法,在重写的方法内会将异步模块放到全局模块中然后等待使用。
  白话版 webpack 构建流程
  时至今日,webpack 的功能集已经非常庞大了,代码量更是非常惊人,源码的学习成本非常高,但是了解 webpack 构建流程又十分有必要,可以帮我们了解构建产物是怎么产生的,以及遇到实际问题时如何下手解决问题。
  思路实现
  简单模拟下 webpack 实现思路: class Compilation {     constructor(options) {         this.options = options;         // 本次编译所有生成出来的模块         this.modules = [];         // 本次编译产出的所有代码块, 入口模块和依赖模块打包在一起成为代码块         this.chunks = [];         // 本次编译产出的资源文件         this.assets = {};     }     build(callback) {         //5.根据配置文件中的`entry`配置项找到所有的入口         let entry = {xxx: "xxx"};          //6.从入口文件出发,调用所有配置的loader规则,比如说loader对模块进行编译         for(let entryName in entry){             // 6. 从入口文件出发,调用所有配置的Loader对模块进行编译             const entryModule = this.buildModule(entryName, entryFilePath);             this.modules.push(entryModule);              //8.等把所有的模块编译完成后,根据模块之间的依赖关系,组装成一个个包含多个模块的chunk             let chunk = {                 name: entryName, // 代码块的名称就是入口的名称                 entryModule, // 此代码块对应的入口模块                 modules: this.modules.filter((module) => module.names.includes(entryName)) // 此代码块包含的依赖模块             };             this.chunks.push(chunk);         }          //9.再把各个代码块chunk转换成一个一个的文件(asset)加入到输出列表         this.chunks.forEach((chunk) => {             const filename = this.options.output.filename.replace("[name]", chunk.name); // 获取输出文件名称             this.assets[filename] = getSource(chunk);         });         // 调用编译结束的回掉         callback(null, {             modules: this.modules,             chunks: this.chunks,             assets: this.assets         }, this.fileDependencies);      }      //当你编译 模块的时候,需要传递你这个模块是属于哪个代码块的,传入代码块的名称     buildModule(name, modulePath) {          // 6. 从入口文件出发,调用所有配置的Loader对模块进行编译, loader 只会在编译过程中使用, plugin则会贯穿整个流程          // 读取模块内容          let sourceCode = fs.readFileSync(modulePath, "utf8");          //创建一个模块对象          let module = {              id: moduleId, // 模块ID =》 相对于工作目录的相对路径              names: [name], // 表示当前的模块属于哪个代码块(chunk)              dependencies: [], // 表示当前模块依赖的模块          }           // 查找所有匹配的loader,自右向左读取loader, 进行转译, 通过loader翻译后的内容一定是JS内容          sourceCode = loaders.reduceRight((sourceCode, loader) => {              return require(loader)(sourceCode);          }, sourceCode);           // 7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理          // 创建语法树, 遍历语法树,在此过程进行依赖收集, 绘制依赖图          let ast = parser.parse(sourceCode, { sourceType: "module" });          traverse(ast, {});          let { code } = generator(ast);           // 把转译后的源代码放到module._source上          module._source = code;          // 再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理          module.dependencies.forEach(({ depModuleId, depModulePath }) => {             const depModule = this.buildModule(name, depModulePath);             this.modules.push(depModule)          });           return module;     } }  function getSource(chunk) {     return `      (() => {       var modules = {         ${chunk.modules.map(           (module) => `           "${module.id}": (module) => {             ${module._source}           }         `       )}       };       var cache = {};       function require(moduleId) {         var cachedModule = cache[moduleId];         if (cachedModule !== undefined) {           return cachedModule.exports;         }         var module = (cache[moduleId] = {           exports: {},         });         modules[moduleId](module, module.exports, require "moduleId");         return module.exports;       }       var exports ={};       ${chunk.entryModule._source}     })();      `; }  class Compiler {     constructor(options) {         this.options = options;         this.hooks = {             run: new SyncHook(),  //会在编译刚开始的时候触发此run钩子             done: new SyncHook(), //会在编译 结束的时候触发此done钩子         }     }      //4.执行`Compiler`对象的`run`方法开始执行编译     run() {         // 在编译前触发run钩子执行, 表示开始启动编译了         this.hooks.run.call();         // 编译成功之后的回掉         const onCompiled = (err, stats, fileDependencies) => {             // 10. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统             for(let filename in stats.assets) {                 fs.writeFileSync(filePath,stats.assets[filename], "utf8" );             }             //当编译成功后会触发done这个钩子执行             this.hooks.done.call();         }         //开始编译,编译 成功之后调用onCompiled方法         this.compile(onCompiled);     }      compile(callback) {         // webpack虽然只有一个Compiler, 但是每次编译都会产出一个新的Compilation, 用来存放本次编译产出的 文件、chunk、和模块         // 比如:监听模式会触发多次编译         let compilation = new Compilation(this.options);         //执行compilation的build方法进行编译 ,编译 成功之后执行回调         compilation.build(callback);     }  }  function webpack(options) {     //1.初始化参数,从配置文件和shell语句中读取并合并参数,并得到最终的配置对象     let finalOptions = {...options, ...shellOptions};      // 2.用上一步的配置对象初始化Compiler对象, 整个编译流程只有一个complier对象     const compiler = new Compiler(finalOptions);      // 3.加载所有在配置文件中配置的插件     const { plugins } = finalOptions;     for(let plugin of plugins){         plugin.apply(compiler);     }      return compiler; }  // webpackOptions webpack的配置项 const compiler = webpack(webpackOptions); //4.执行对象的run方法开始执行编译 compiler.run();
  为什么 Rollup 构建产物很干净? rollup 只对 ESM 模块进行打包,对于 cjs 模块也会通过插件将其转化为 ESM 模块进行打包。所以不会像 webpack 有很多的代码注入。 rollup 对打包结果也支持多种 format 的输出,比如:esm、cjs、am 等等,但是 rollup 并不保证代码可靠运行,需要运行环境可靠支持。比如我们输出 esm 规范代码,代码运行时完全依赖高版本浏览器原生去支持 esm,rollup 不会像 webpack 一样注入一系列兼容代码。 rollup 实现了强大的 tree-shaking 能力。
  为什么 Vite 可以让代码直接运行在浏览器上?
  前文我们提到,在开发环境时,我们使用 vite 开发,是无需打包的,直接利用浏览器对 ESM 的支持,就可以访问我们写的组件代码,但是一些组件代码文件往往不是 JS 文件,而是  .ts  、.tsx  、.vue   等类型的文件。这些文件浏览器肯定直接是识别不了的,vite 在这个过程中做了些什么?
  我们以一个简单的 vue 组件访问分析一下: // index.html                         Vite + Vue                      // /src/main.js import { createApp } from "vue" import App from "./App.vue" createApp(App).mount("#app");  // src/App.vue 
  在浏览器中打开页面后,会发现浏览器对入口文件发起了请求:
  我们可以观察到 vue 这个第三方包的访问路径改变了,变成了 node_modules/.vite  下的一个 vue 文件,这里真正访问的文件就是前面我们提到的,vite 会对第三方依赖进行依赖预构建  所生成的缓存文件。
  另外浏览器也对 App.vue 发起了访问,相应内容是 JS:
  返回内容(做了部分简化,移除了一些热更新的代码):  const _sfc_main = {     name: "App" } // vue 提供的一些API,用于生成block、虚拟DOM import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/vue.js?v=b618a526"  function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {     return (_openBlock(), _createElementBlock("h1", null, "App")) } // 组件的render方法 _sfc_main.render = _sfc_render; export default _sfc_main;
  总结:当用户访问 vite 提供的开发服务器时,对于浏览器不能直接识别的文件,服务器的一些中间件会将此类文件转换成浏览器认识的文件,从而保证正常访问。 参考资料
  [1] Grunt: https://www.gruntjs.net/
  [2] Gulp: https://www.gulpjs.com.cn/
  [3] browserify: https://browserify.org/
  [4] webpack: https://webpack.docschina.org/
  [5] rollup: https://rollupjs.org/guide/zh/
  [6] esbuild: https://esbuild.github.io/
  [7] vite: https://cn.vitejs.dev/guide/
  [8] 2021 前端构建工具排行: https://risingstars.js.org/2021/zh#section-build
  [9] 前端构建工具简史: https://juejin.cn/post/7085613927249215525
  [10] ESM 实现原理: https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-pe/
  [11] Webpack 核心原理 : https://mp.weixin.qq.com/s/SbJNbSVzSPSKBe2YStn2Zw
  关注「字节前端 ByteFE」公众号,追更不迷路!

孩子的学习道路上,家长到底要怎样引导呢?每个家长都关注自己孩子的学习,都希望自己的孩子学习成绩优异。要实现这一美好的愿望,确实需要家长用心引导。对孩子学习的引导,常常是孩子上了小学以后,家长才重视。实际上,对孩子学习的引迈腾值得入手吗?迈腾这个名字其实非常有中国特色,因为了解大众的人知道,迈腾其实就是帕萨特,只不过他们是两个不同恶版本而已(迈腾是B8,在海外市场还是叫帕萨特),我不太理解大众这样做的用意,难道就是越发达的国家,越不认可支付宝和微信支付,为什么?不知道你从哪看出越发达国家越不认可支付宝和微信支付?我看到的报道基本上是好多发达国家的人到中国居住一段时间后,都非常喜欢中国的支付宝和微信支付方式,不必携带现金,感觉到非常方便快捷尾盘突然拉升,一般是不是接下来都会跌?股票尾盘偷袭拉升的叫非奸即盗,尾盘拉升的股票后期95的概率都会下跌,为什么会这样呢?首先股票市场真正的强势股票,真正的妖股都是在早盘开盘之后就会拉升,真正妖股会早盘10点之前强势封十年前投资房子的都发财了,现在投资什么会让十年后财富自由?十年前投资房子的都发财了,现在投资什么会让十年后财富自由?人长前后眼,何须聚宝盆,瞎猫还会碰上死耗子的,智汉瞒痴汉,痴汉总不知,智汉做驴子,反被痴汉骑。有谁发财了,就不用接意他了,老板拖欠工资,一直说过几天给,拖欠五个多月了,该怎么办?没别的办法,只能拿起你手中的武器法律。证据合同,以事实为依据,以法律为准绳。用法律说话,收集一切的证据,到仲裁讨回你的工钱,仲裁是不收费的。劝诫一些老板,识时务者为俊杰,不要将错就90后月薪多少才可以撑起一个家?回答90后月薪多少才可以撑起一个家,看你在几线城市,上有老下有小在一线城市,父母没有退休工资,夫妻月薪不能低于30000块,高档次的消费只够开支,难有结余,如果生活在三四线城市,两生女儿的好处,生儿子的想象不到,是这样吗?其实养育下一代,生儿子和女儿的意义都一样。生养生养,生下来,好好陪伴,好好养育,让他们有更好的能力去打拼自己的世界。中国有句古话养儿防老,但是又有多少人在养育孩子后,真的能享受到孩为什么戴上助听器后,感觉自己的声音变了呢?在听力学上叫做堵耳效应,其实我们正常情况下,如果把手指堵住耳道,也会出现自己说话声音变大的现象,佩戴上助听器自己说话的声音是通过骨传导放大,也就是通过颅骨的振动,别人说话的声音不通天津有许多相声名家,例如刘文亨等,为什么感觉知名度仅限于天津??介回说谁(sei),画说仓圣将军赵云赵(吃没了)龙,怀揣阿斗,全凭跨下马吧,脏中的介根抢,撒了个气出气入刘老爷子的活接地气这个问题主要是时代造成的,刘文亨他们这代人,成名一般都是在为什么感觉山东人在全国各地的口碑都很好?有一年我去山东泰山,在去之前朋友告诉了我一个电话号码,是他一个泰山当地朋友的。他们是在海南旅游时跟同一个旅行团认识的,相谈甚欢,于是就相互留下了电话,期间联系过几次,但他们在海南一
泰州苏王张雪迎到中森明菜分析苦相女星的特点客观地讲,张雪迎的长相决定了她单纯的邻家女孩风格,准确的说是贫苦的邻家女孩风格首先面部软组织过少,面中不够饱满甚至偏平,嘴唇很薄,另外就是眉骨肌肉走势向下,静态还是笑起来眉尾都是低朴彩英allblack造型,纯黑男装变身酷飒女总裁,可爱姿势粉丝美哭10月20日,韩娱顶流女团BLACKPINK成员朴彩英以一身纯黑色装扮,出席电影Remember的首映礼活动。这位舞台上的澳洲野玫瑰摇身一变allblack酷飒女总裁,引得粉丝们直为什么日本酒吧的妈妈桑喜欢穿和服万景路本尼迪克特在菊与刀中阐述日本人的民族性格时曾说日本人喜欢肉体享乐,却又不严肃对待。他们只把肉体享乐当作一种纯粹的艺术来培养可谓一语中的。以我们日常和日本人接触所感受到的日本人阳光的声音(外两首)经过多少路程萦绕耳边细语缠绵温婉的音律漫过草树和花朵带来天使的温暖每一声呼唤都追赶孤独的背影每一句话都明亮阴黯角落寥廓的人间天安门向往已久的思绪沿着琉璃瓦脊攀援古老和现代的奇迹看,你害怕失败吗?你认为哪些人生经验越早知道越好你为啥会害怕失败?因为你失败的次数太少了。失败少的人他们还是即使如此成功的时段比较多或者即或一直居于清高的状态。成功多的人害怕失败因为他们已经吃得来了青未了花语张程程我一直都是爱花的。小时候便是如此,印象最深的就是夏天成片的小地雷花,紫红色黄色的白色的还有混合色,我最喜欢把它们一朵一朵摘下来,再把花末端的小黑种子轻轻一拉,小种子就和花朵分开了,试水作文写身边的人,企图让学生明白作文离不开观察生活可爱的女孩在我现在的班里,有这样一位学生,她是一个很可爱的女孩。她中等个子,瓜子脸,马尾辫。喜欢笑,有一双明亮会说话的眼睛。肤色不白,也不爱穿着打扮。就是这样一个普通女孩,刚上初中大舌头和大耳朵你说我是大舌头,说都不会话。我说你是大耳朵,话都不会听。舌头和耳朵的矛盾由来已久,都觉得自己的声音好听,都觉得别人的耳朵灵敏。都想当说客,都不想当听众。都觉得自己是天籁,别人是噪音郑秀妍晒四宫格自拍,黑色蕾丝裙优雅迷人,格子深V长裙性感美艳头条创作挑战赛郑秀妍晒最新美照四宫格营业,一头长发扎起,穿着黑色蕾丝长裙,好优雅高贵,手帖脸比爱心,状态好好,脸上看起来胶原蛋白满满。笑的也是非常明媚灿烂了。这一袭黑色蕾丝裙真的是配上峰米激光电视Cinema2看大片才叫过瘾如果你总是嫌弃家里电视屏幕太小,看大片不过瘾,完全没有身临其境的观影体验,那么不妨配上峰米这台激光电视Cinema2。相信有它在手,能瞬间提升你的观影体验。峰米激光电视Cinema顶级模特杰西卡斯塔姆(JessicaStam)拍摄的时尚杂志杰西卡斯塔姆(JessicaStam)拍摄哈萨克斯坦版时尚芭莎级模特杰西卡斯塔姆为时尚芭莎哈萨克斯坦登上了三个封面。首先,金发女郎穿着香奈儿礼服(上图)。第二个,杰西卡为范思哲连衣