1. 程式人生 > >webpack等bundler是如何工作的-簡化版本

webpack等bundler是如何工作的-簡化版本

others pen div 頁面 product 負責 ati MinIP root

webpack- why and how

首先不要被webpack做的復雜花哨的工作所迷惑,到底webpack是個啥?一句話,webpack是一個module bundler(模塊打包器)。多一句話,webpack通過將多個js文件或者其他assets整合進一個大的bundle.js文件中,允許我們在瀏覽器中使用javascript module功能。而所有類似翻譯es6/es7為es5,或者使用css module功能僅僅為webpack提供的一些額外功能。但是我們永遠不要忘記其核心功能:一個允許在瀏覽器中使用js模塊的打包器

由於webpack有著非常強大的plugin/loader生態系統,這往往使得初學者眼花繚亂,從而對webpack是什麽這個核心問題有些糊塗,因為webpack貌似無所不能,能做太多太多。雖然透過plugin機制我們獲得了很多重要的功能本身是很棒的,但是我們還是應該聚焦一下:為什麽webpack會存在:module bunlding.

webpack是一個命令行工具,用於生成由code和其他file組成的bunldle。webpack本身並不運行於server或者說browser,webpack命令行工具只運行於web app的構建過程。webpack從一個entry.js文件開始,將其依賴的所有js或者其他assets(通過loader)打包成一個巨大的文件。

隨後這個大文件將被從server傳遞到客戶端瀏覽器運行。註意:browser和server並不會關心該文件是否由webpack所生成,browser/server將該bundle文件當作其他文件一樣來看待和處理。

webpack-dev-server vs webpack cli

webpack-dev-server是一個和webpack-cli不同的工具,它是一個基於node/express的dev server.當該server運行時,我們實際上從該server的一個端口下load app(bundle)並在瀏覽器中運行。當然該server還能提供其他一些非常棒的功能,比如Hot module reloading.

Why

傳統server rendered app

傳統的web app實際上是server端渲染的頁面,這意味著瀏覽器僅僅是一個html瀏覽器,所有的邏輯都存在於server端。server端將一個靜態的html頁面傳回客戶端瀏覽器,瀏覽器只負責渲染靜態的html/css.這也是為什麽當你導航在server rendered app的不同頁面時瀏覽器總會刷新的原因。

Spa: single page app

自從js ajax興起後,單頁應用就開始慢慢崛起,因為它的無刷新特性給與了很棒的用戶體驗。

Dynamic Spas mean more code in the browser

當我們說dynamic時我們是指有越來越多的以javascript形式存在的logic運行於瀏覽器中。而我們server side render apps僅會吐出一些並非dynamic的static page,這時所謂的dynamic僅發生在server端用於產生這個static page,而一旦該page產生並且傳遞到browser了,就基本上是靜態的了(因為並沒有很多的javascript代碼)

How

如何管理越來越多的瀏覽器端邏輯是一個重要挑戰,而webpack就是為了應對這種web開發越來越多邏輯從服務端向客戶端轉移的現狀的。。。

那麽,我們要問,為什麽spa單頁應用就會稱為問題呢?

一個很重要的原因是:我們需要將復雜的js代碼劈成更小的文件以便app更好的工作。

我們可以將所有邏輯寫成一個js文件,然而,可以想象這將稱為開發者的噩夢。沒有人能知道應用是如何工作的,想開發新的功能難於上青天。這時,我們就需要將復雜的邏輯分到不同的chunk中,也就是根據功能劃分將邏輯劃分到多個js文件中去。這就是一個“模塊化”的開發模式。

你可能又會想,那我們就把一個大的文件打散成多個js文件唄。。但問題是瀏覽器並不知道這些文件之間的依賴關系,因此我們需要一個模塊管理工具,以便在瀏覽器端支持模塊化。這就是webpack/browserfy帶來的價值。

在server端node環境中,支持內置的module resolver,我們可以require一個module來使用它,然而瀏覽器並不具備這個require一個module的能力,也就是說browser是不懂require,import的!

技術分享圖片

https://adamisntdead.com/lets-write-a-module-bundler/

咱們直接上一個網上找的最簡單的bundler代碼,了解下具體工作原理

//  Hello! Welcome, welcome, it‘s great to have you here!
//  Today we‘re going to be building a really simple Javascript module
//  bundler!
//
//  Before we start, I want to give a few acknowledgements, for which this
//  source is heavily based on.
//
//  * Unbundling the JavaScript module bundler - Luciano Mammino
//    http://loige.link/bundle-dublinjs
//
//  * Minipack - Ronen Amiel
//    https://github.com/ronami/minipack
//
//  Okay, lets get started!
//


//  Let‘s start with what a module bundler actually is.
//
//  You may have used tools such as browserify, webpack, rollup or one of
//  many others, but a module bundler is a tool that takes pieces of
//  javascript and takes them and their dependencies and makes it into a
//  single file, usually for use in the browser.
//
//  It usually starts with an entry file, and from their bundles up all of
//  the code needed for that entry file.
//
//    + - - - - - - - - - - - - - - - - - +
//                   app.js
//    |                 +                 |
//         +------------+------------+
//    |    |            |            |    |            +-----------+
//         v            v            v                 |           |
//    | log.js      utils.js      dom.js  |----------->| bundle.js |
//         |            |             |                |           |
//    |    +------+-----+      +------+   |            +-----------+
//                |            |
//    |           v            v          |
//             lodash       jquery
//    + - - - - - - - - - - - - - - - - - +
//
//   There are 2 main stages of a bundler:
//
//   1. Dependency resolution
//   2. Packing
//
//   Starting from an entry point (above it‘s `main.js`), the goal of
//   dependency resolution is to look for all of the dependencies of your
//   code - that is, other pieces of code that it needs to function - and
//   construct a graph (called a dependency graph) like the above.
//
//   Once this is done, you can then pack, or convert your dependency graph
//   into a single file that you can use.


//   Let‘s start out our code with some imports (I will give the reasoning
//   later)

const detective = require(‘detective‘)
const resolve = require(‘resolve‘).sync
const fs = require(‘fs‘)
const path = require(‘path‘)

//   The first thing we have to do is think up how we want to represent a
//   module during the dependency resolution phase.
//   We are going to need four things:
//
//   * The name and an identifier of the file
//   * Where the file came from (in the file system)
//   * The code in the file
//   * What dependencies that file needs.
//
//   The graph structure gets built up through the recursive ‘what
//   dependencies‘ question.
//
//   In Javascript, the easiest way to represent such a set of data would
//   be an object, so thats what‘s gonna happen.

let ID = 0
function createModuleObject(filepath) {
  const source = fs.readFileSync(filepath, ‘utf-8‘)
  const requires = detective(source)
  const id = ID++

  return { id, filepath, source, requires }
}

//
//   Looking at this `createDependencyObject` function, the notable part
//   is the call to a function called `detective`.
//
//   Detective is a library that can "Find all calls to require() no
//   matter how deeply nested", and using it means we can avoid doing our
//   own AST traversal!
//
//   One thing to note (and this is the same in almost all module
//   bundlers), if you try to do something weird like
//
//   ```
//   const libName = ‘lodash‘
//   const lib = require(libName)
//   ```
//
//   It will not be able to find it (because that would mean executing the
//   code).


//   So what does running this function on the path of a module give?
//
//
//                                             {
//   ╭-----------------------------------╮       id: 0,
//   | ◎ ○ ○         app.js              |       filepath: ‘/Users/john/app.js‘,
//   +-----------------------------------+       requires: [ ‘./log‘, ‘./utils‘ ],
//   | const log = require(‘./logging‘)  |       source: `
//   | const util = require(‘./utils‘)   |         const log = require(‘./logging‘)
//   |                                   +---->    const util = require(‘./utils‘)
//   | log(‘hello world!‘)               |
//   |                                   |         log(‘hello world!‘)
//   |                                   |       `
//   +-----------------------------------+     }
//


//   Whats next?
//   Dependency resolution!!1!11!!!1!
//
//   Okay, not quite yet - I first want to talk about a thing called a
//   module map.
//
//   When you import modules in node, you can do relative imports, like
//   `require(‘./utils‘)`. So when your code calls this, how does the
//   bundler know what is the right ‘./utils‘ file when everything is
//   packaged?
//
//   That is the problem the module map solves.
//
//   Our module object has a unique `id` key which will be our ‘source of
//   truth‘. So when we are doing our dependency resolution, for each
//   module, we will keep a list of the names of what is being required
//   along with their id, so we can get the correct module at runtime.
//
//   This also means we can store all of the modules in a non-nested
//   object, using the id as a key!
//
//
//
//            +-------------+                      +-----------+
//            | Modules Map |                      |  Modules  |
//       +----+-----------+-+---+            +-----+-----------+----+
//    +--+--->./utils     |  2 <+--+         |  2  |    { ... }     |
//    |  +----------------+-----+  |         +-----+----------------+
//    |  |    ./logger    |  3  |  |         |  3  |    { ... }     |
//    |  +----------------+-----+  |         +-----+----------------+
//    |  |     moment     |  4  |  |         |  3  |    { ... }     |
//    |  +----------------+-----+  |         +-----+----------------+
//    |  |      ...       | ... |  |         | ... |      ...       |
//    |  +----------------+-----+  |         +-----+----------------+
//    |                            |
//    |                            |
//  argument to                  module
//    require                  object‘s id

function getModules(entry) {
  const rootModule = createModuleObject(entry)
  const modules = [rootModule]

  // Iterate over the modules, even when new 
  // ones are being added
  for (const module of modules) {
    module.map = {} // Where we will keep the module maps

    module.requires.forEach(dependency => {
      const basedir = path.dirname(module.filepath)
      const dependencyPath = resolve(dependency, { basedir })

      const dependencyObject = createModuleObject(dependencyPath)

      module.map[dependency] = dependencyObject.id
      modules.push(dependencyObject)
    })
  }

  return modules
}

//   Okay, so there is a fair amount going on in the `getModules`
//   function.
//   It‘s main purpose is to start at the root/entry module, and look for
//   and resolve dependencies recursively.
//
//   What do I mean by ‘resolve dependencies‘?
//   In node there is a thing called the `require.resolve`, and it‘s how
//   node figures out where the file that you are requiring is. This is
//   because we can import relatively or from a `node_modules` folder.
//
//   Lucky for us, there‘s an npm module named `resolve` which implements
//   this algorithm for us! We just have to pass in the argument that
//   would be for require and the base url, and it will do all the hard
//   work for us :)
//
//   So we are doing this resolution for each dependency of each module in
//   the project.
//
//   We are also creating that modules map `map` that I mentioned earlier.
//
//   At the end of the function, we are left with an array named `modules`
//   which will contain module objects for every module/dependency in our
//   project!
//
//   Now that we have that, we can move on to the final step, packing!


//   In the browser there is no such thing as modules (kind of), but that
//   means there is no require function, and no `module.exports`, so even
//   though we have all of our dependencies, we currently have no way to
//   use them as modules.
//
//   Enter the factory function.
//   A factory function is a function (that‘s not a constructor) which
//   returns an object.
//   It is a pattern from object oriented programming, and one of its uses
//   is to do encapsulation and dependency injection.
//
//   Sound good?
//
//   Using a factory function, we can both inject our own `require`
//   function and `module.exports` object that can be used in our bundled
//   code and give the module it‘s own scope.
//
//                  +-------------------------------+
//                  |       Factory Function        |
//                  +-------------------------------+
//                  |                               |
//                  |    (require, module) => {     |
//                  |        /* Module source */    |
//                  |    }                          |
//                  +-------------------------------+
//
//   I am now going to show you the `pack` function, and I will explain
//   the rest after.

function pack(modules) {
  const modulesSource = modules.map(module => 
    `${module.id}: {
      factory: (module, require) => {
        ${module.source}
      },
      map: ${JSON.stringify(module.map)}
    }`
  ).join()

  return `(modules => {
    const require = id => {
      const { factory, map } = modules[id]
      const localRequire = name => require(map[name])
      const module = { exports: {} }

      factory(module, localRequire)

      return module.exports
    }

    require(0)
  })({ ${modulesSource} })`
}

//   Most of that is just template literals of javascript, so let‘s
//   discuss what it‘s doing.
//
//   First up is `modulesSource`. Here, we are going through each of the
//   modules and transforming it into a string of source.
//   So what is the output like for a module object?
//
//  {
//    id: 0,                                  0: {
//    filepath: ‘/Users/john/app.js‘,           factory: (module, require) => {
//    requires: [ ‘./log‘, ‘./utils‘ ],           const log = require(‘./logging‘)
//    map: { ‘./log‘: 1, ‘./utils‘: 2 }           const util = require(‘./utils‘)
//    source: `                          ---->
//      const log = require(‘./logging‘)          log(‘hello world!‘)
//      const util = require(‘./utils‘)         },
//                                              map: { ‘./log‘: 1, ‘./utils‘: 2 }
//      log(‘hello world!‘)                   }
//    `
//  }
//
//   Now it‘s a little hard to read, but you can see that the source is
//   encapsulated and we are providing `modules` and `require` using the
//   factory function as I mentioned before.
//
//   We are also including the modules map that we constructed during the
//   dependency resolution.
//
//   Next in the function we join all of these up to create a big object
//   of all the dependencies.


//   The next string of code is an IIFE, which means when you run that
//   code in the browser (or anywhere else), the function will be ran
//   immediately. IIFE is another pattern for encapsulating scope, and is
//   used here to so we don‘t polute the global scope with our `require`
//   and `modules.


//   You can see that we are defining two require functions, `require` and
//   `localRequire`.
//
//   Require accepts the id of a module object, but of course the source
//   code isn‘t written using ids, we are using the other function
//   `localRequire` to take any arguments to require by the modules and
//   convert them to the correct id.
//   This is using those module maps!
//
//   After this, we are defining a `module object` that the module can
//   populate, and passing both functions into the factory, after which we
//   return `module.exports`.
//
//   Lastly, we call `require(0)` to require the module with an id of 0,
//   being our entry file.
//
//   And thats it!
//   That is our module bundler 100% complete!

module.exports = entry => pack(getModules(entry))

//   So we now have a working module bundler.
//
//   Now this probably shouldn‘t be used in production, because it‘s
//   missing loads of features (like managing circular dependency, making
//   sure each file gets only parsed once, es-modules etc...) but has
//   hopefully given you a good idea on how module bundlers actually work.
//
//   In fact, this one works in about 60 lines if you remove all of the
//   source code!
//
//   Thanks for reading and I hope you have enjoyed a look into the
//   workings of our simple module bundler!

https://stackoverflow.com/questions/40562031/webpack-how-does-webpack-work

webpack等bundler是如何工作的-簡化版本