手写webpack核心原理,支持typescript的编译和循环依赖问题的解决

这篇具有很好参考价值的文章主要介绍了手写webpack核心原理,支持typescript的编译和循环依赖问题的解决。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

 

主要知识点

  1. babel读取代码的import语句
  2. 算法:bfs遍历依赖图
  3. 为浏览器定义一个require函数的polyfill
  4. 算法:用记忆化搜索解决require函数的循环依赖问题

Quick Start

GitHub:https://github.com/Hans774882968/mini-webpack

npm install
npm run bundle
修改index.html依赖的js文件路径(bundle_ts.js),复制到dist文件夹,然后点击打开index.html。

依赖

npm i @babel/parser
npm i @babel/traverse
npm i -D @types/babel__traverse
npm i @babel/core @babel/preset-env
npm i -D @babel/preset-typescript
npm i -D @types/babel__core
npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser
npm i typescript@4.7.4

注意点:

  1. 配置eslint后记得重启一下vscode,IDE提示才会生效。
  2. 我们的命令在2022-08-27下载了@babel/core7.18.13,对应的ts版本要指定为typescript@4.7.4,否则运行代码会报错。

引言

主要是借鉴参考链接1来实现一个mini-webpack,但在功能上有所超越:

  1. 根据入口文件的拓展名,决定用ts或js来编译。
  2. 借鉴参考链接3,用“记忆化搜索”解决循环依赖问题。

最大的缺憾是不清楚ts-loader怎么实现,因此这里编译ts的做法是直接判定入口文件的扩展名为.ts,然后用babel实现。

因为参考链接1写得很清晰了,本文仅定位为一个额外补充,不会写得很详细。

初始化项目

npm init
tsc --init

tsconfig.json主要需要设置:

"compilerOptions": {
  "module": "commonjs",
},
"include": [
  "bundle.ts"
]

样就能用tsc命令编译入口文件了。

接下来给package.json加一个命令:"bundle": "tsc && node bundle.js",以后可以直接npm run bundle模拟打包命令了。

目前除了在nodejs代码里用'@babel/preset-typescript'插件以外,不知道怎么快速方便地编译src文件夹下的ts,只好:先手工修改tsconfig.jsonincludecompilerOptions.module,接着tsc编译,最后还原tsconfig.json

读取单个文件

getModuleInfo函数主要分析文件的依赖和完成代码转换。

  1. 我们需要分析文件的import语句,把依赖的文件(相对路径)转换为相对于项目根目录的路径(下称“绝对路径”)。使用babel相关的库@babel/parser@babel/traverse@babel/core完成。
  2. 代码转换用@babel/coretransformFromAst方法完成。
  3. 我们需要保证生成的js的模块规范是commonjs。对于编译js的情况不需要特别指明,而编译ts的情况需要指明插件:plugins: ['@babel/plugin-transform-modules-commonjs'](参考链接5)。
如何支持typescript的编译

只需要修改@babel/parserparse方法和@babel/coretransformFromAst方法的调用方式。

需要用到@babel/preset-typescript这个插件。

@babel/preset-typescript没有@babel/preset-env方便,

需要指明filename属性和@babel/plugin-transform-modules-commonjs插件。

相关语句:

const ast = parser.parse(body, {
  sourceType: 'module',
  plugins: getType() === 'ts' ? ['decorators-legacy', 'typescript'] : []
});

babel.transformFromAst(ast, undefined,
  getType() === 'ts' ?
    {
      presets: ['@babel/preset-typescript'],
      filename: file,
      plugins: ['@babel/plugin-transform-modules-commonjs']
    } :
    { presets: ['@babel/preset-env'] },
  (err, result) => {
    if (err) reject(err);
    resolve(result as babel.BabelFileResult);
  }
);

遍历依赖图

parseModule函数。因为循环依赖也只不过是形成递归,所以依赖图不局限于DAG,可以是任意有向图。所以只需要用bfs遍历一下(这里更正参考链接1的一处小错误,遍历算法不是递归而是bfs)。

  1. parseModule函数中的for循环for (const { deps } of a)用到了它会继续遍历新加入的元素的特性,不能替换为forEach,是js实现bfs的最简方案。
  2. parseModule函数中的await Promise.all是循环中使用async/await的解决方案(参考链接4)。

parseModule函数的输出为depGraph哈希表,其一个对象的deps属性应该设计为一个哈希表,而非直接设计为数组,下文会解释原因。

生成单个文件

getBundle函数把上面生成的depGraph哈希表扔进代码模板里,这就是打包结果。

为了在浏览器环境给出一个合法的commonjs的polyfill(这里只需要给出requireexports对象),我们在代码模板中定义了自己的require函数。对于一个代码文件来说,其返回值为这个文件的exports对象,其副作用为把整个文件的代码执行了一遍。

`;(function (graph) {
  var exportsInfo = {};
  function require (file) {
    if (exportsInfo[file]) return exportsInfo[file];
    function absRequire (relPath) {
      return require(graph[file].deps[relPath]);
    }
    var exports = {};
    exportsInfo[file] = exports;
    (function (require, exports, code) {
      eval(code);
    })(absRequire, exports, graph[file].code);
    return exports;
  }
  require('${entryPath}');
})(${depGraph});`

值得注意的是,这个require函数实际上是一个递归函数。在eval(code)时可能产生递归。

depGraph哈希表的一个对象的deps属性为什么设计为一个哈希表,而非直接设计为数组?因为待执行的代码中所有的路径都是相对路径,我们需要用graph[file].deps[relPath]这样的方式把它转换为绝对路径。为了完成这个转换,我们还需要设计absRequire函数,它只不过起到一个拦截器的作用。

如何解决循环依赖

此时我们如果打包一个含有循环依赖的入口文件,运行时会栈溢出。以最简单的情况为例:a模块的a函数引用b模块的b函数,b模块的b函数引用a模块的a函数。

怎么解决呢?根据参考链接3,我们可以用“记忆化搜索”的思路,开一个全局变量var exportsInfo = {};。并在exports对象生成以后,立即exportsInfo[file] = exports;。上文案例中,b模块获得的a模块的exports对象的值是空的,但因为对象的浅拷贝特性,对象地址是正确的,在require函数解析a模块完毕后,b模块也就能获得a模块的exports对象的正确值了。

相信对acmer来说这个算法很经典,没有相关背景的话可以尝试在浏览器打断点帮助理解。

入口

main函数在最开始读取配置(模仿webpack.config.js),在最后把getBundle生成的单个文件写入文件系统。文章来源地址https://www.toymoban.com/news/detail-796583.html

参考链接

  1. 手写webpack核心原理,再也不怕面试官问我webpack原理:手写webpack核心原理,再也不怕面试官问我webpack原理 - 掘金
  2. @babel/core官方文档:@babel/core
  3. webpack如何解决循环依赖:webpack如何解决循环依赖 - 知乎
  4. 循环中使用async/await的解决方案:五种在循环中使用 async/await 的方法 - 知乎
  5. @babel/plugin-transform-modules-commonjs · Babel

到了这里,关于手写webpack核心原理,支持typescript的编译和循环依赖问题的解决的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 手写Webpack-Plugin

    通过插件我们可以扩展webpack,使webpack可以执行更广泛的任务,拥有更强的构建能力。 webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能

    2024年01月21日
    浏览(34)
  • 手写一个webpack插件(plugin)

    熟悉 vue 和 react 的小伙伴们都知道,在执行过程中会有各种生命周期钩子,其实webpack也不例外,在使用webpack的时候,我们有时候需要在 webpack 构建流程中引入自定义的行为,这个时候就可以在 hooks 钩子中添加自己的方法。 创建插件 webpack 加载 webpack.config.js 中所有配置,此

    2024年02月08日
    浏览(46)
  • [编译原理]DO-WHILE循环语句的翻译程序设计(LR(1)方法、输出四元式)C++实现

    初始条件: ​ 理论:完成编译原理,数据结构、高级编程语言、汇编语言等相关课程的学习,基于计算机专业知识进行课程设计。 ​ 实践:计算机实验室提供计算机及软件环境。如果自己有计算机及环境也可以在其上进行设计任务。 要求完成的主要任务: (包括课程设计工

    2024年02月03日
    浏览(53)
  • 使用webpack5+TypeScript+npm发布组件库

            作为一只前端攻城狮,没有一个属于自己的组件库,那岂不是狮子没有了牙齿,士兵没有了武器,姑娘没有了大宝SOD蜜,你没有了我....         言归正传,下面将给大家介绍如何通过webpack5编译一个TS组件发布到NPM平台。         1、通过webpack5初始化一个typescript环

    2024年04月16日
    浏览(41)
  • 手写Springboot核心流程

    目录 Springboot启动流程 核心代码 验证效果 创建Spring容器, 扫描并启动 容器选择Tomcat/Jetty 创建DispatchServlet, 与spring容器绑定 将DispatchServlet添加到Tomcat 启动Tomcat 1. 首先, 创建两个module  2. maven依赖 springboot模块依赖 user模块依赖 3. springboot核心配置类  4. Import自动配置类  spi注入

    2024年02月01日
    浏览(38)
  • webpack:系统的了解webpack一些核心概念

    webpack 如何处理应用程序? 从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块(chunk)组合成一个或多个 bundles 何为webpack模块chunk? ES2015 import 语句 CommonJS require() 语句 AMD define 和 require 语句 css/sass/less 文件中的 @import 语句。 stylesheet url(…

    2024年02月07日
    浏览(37)
  • 【框架源码】手写Spring框架IOC容器核心流程

    要是想要了解Spring IOC底层,首先我们就得先了解什么是IOC。 IOC就是控制反转,把对象创建和对象之间的调用过程交给Spring进行管理。 使用IOC目的就是之前我们创建对象的方式都是用new的方式创建,这个方式有个缺点,被创建对象的类的位置一旦被改动,new就会报错,类与类

    2024年02月06日
    浏览(56)
  • 手写axios源码系列一:axios核心知识点

    最近从头搭建了一个vue小项目,想使用 axios 作为请求接口的第三方库。结果使用了 axios 这么长时间,想封装一下 axios ,也就是大部分项目中的 src/utils/request.js 文件,我居然无法默写出来,create 和 interceptors 这样的 api 都不知道怎么使用;所以决定深入一下 axios 源码,一探究

    2023年04月22日
    浏览(52)
  • Webpack vs Vite的核心差异

    Webpack : Webpack的构建速度相对较慢,尤其在大型项目中,因为它需要分析整个依赖图,进行多次文件扫描和转译。 Vite : Vite以开发模式下的极速构建著称。它利用ES模块的特性,只构建正在编辑的文件,而不是整个项目。这使得它在开发环境下几乎是即时的。 Webpack : Webpack通常

    2024年02月09日
    浏览(30)
  • 手写模拟SpringBoot核心流程(二):实现Tomcat和Jetty的切换

    上一篇文章我们聊到,SpringBoot中内置了web服务器,包括Tomcat、Jetty,并且实现了SpringBoot启动Tomcat的流程。 那么SpringBoot怎样自动切换成Jetty服务器呢? 接下来我们继续学习如何实现Tomcat和Jetty的自动切换。 将BerSpringApplication类中startTomcat写到TomcatWebServer实现类中。 JettyWebServe

    2024年02月12日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包