【前端】:模块化 - 打包技术

时间:2022-07-24
本文章向大家介绍【前端】:模块化 - 打包技术,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
1. 核心知识回顾
  1.1. globalThis
  1.2. ES 模块化语法回顾
    1.2.1. 导入
    1.2.2. 导出
    1.2.3. 动态绑定
  1.3. UMD
  1.4. 为什么 ES 模块比 CommonJS 更好?
  1.5. 什么是 "tree-shaking"?
  1.6. mjs 是什么?
  1.7. unpkg 是什么?
  1.8. pkg.module 是什么?
  1.9. commonjs2 是什么?
2. 构建一个库
  2.1. 构建需求?
  2.2. 用 webpack 构建
  2.3. 用 Rollup.js 构建?
  2.4. 用 father-build 构建?

1. 核心知识回顾

1.1. globalThis

  • 在以前,从不同的 JavaScript 环境中获取全局对象需要不同的语句。在 Web 中,可以通过 window、self 或者 frames 取到全局对象,但是在 Web Workers 中,只有 self 可以。在 Node.js 中,它们都无法获取,必须使用 global
  • 在松散模式下,可以在函数中返回 this 来获取全局对象,但是在严格模式和模块环境下,this 会返回 undefined。You can also use Function('return this')(), but environments that disable eval(), like CSP in browsers, prevent use of Function in this way.
  • globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身)。不像 window 或者 self 这些属性,它确保可以在有无窗口的各种环境下正常工作。所以,你可以安心的使用 globalThis,不必担心它的运行环境。为便于记忆,你只需要记住,全局作用域中的 this 就是 globalThis。

1.2. ES 模块化语法回顾

1.2.1. 导入

  • 命名导入
// Import a specific item from a source module, 
// with its original name.
import { something } from './module.js';

// Import a specific item from a source module, 
// with a custom name assigned upon import.
import { something as somethingElse } from './module.js';
  • 命名空间导入
// Import everything from the source module as an object 
// which exposes all the source module's named exports as properties 
// and methods.
import * as module from './module.js'
  • 默认导入
// Import the default export of the source module.
import something from './module.js';
  • 空导入
// Load the module code, but don't make any new objects available.
// This is useful for polyfills, or when the primary purpose of the imported code 
// is to muck about with prototypes.
import './module.js';
  • 动态导入
// This is useful for code-splitting applications and using modules on-the-fly.
import('./modules.js').then(({ default: DefaultExport, NamedExport })=> {
  // do something with modules.
})

1.2.2. 导出

  • 命名导出
// Export a value that has been previously declared:
const something = true;
export { something };

// Rename on export:
export { something as somethingElse };

// this works with `var`, `let`, `const`, `class`, and `function`
export const something = true;
  • 默认导出
// Export a single value as the source module's default export:
//   1. This practice is only recommended if your source module only has one export.
//   2. It is bad practice to mix default and named exports in the same module, 
//      though it is allowed by the specification.
export default something;

1.2.3. 动态绑定

ES modules export live bindings, not values, so values can be changed after they are initially imported as per.

incrementer.js:

// incrementer.js
export let count = 0;

export function increment() {
  count += 1;
}

main.js:

// main.js
import { count, increment } from './incrementer.js';

console.log(count); // 0

increment();
console.log(count); // 1

increment();
console.log(count); // 2

index.html:

<!DOCTYPE html>
<html>
    <body>
        <script type="module" src="./incrementer.js"></script>
        <script type="module" src="./main.js"></script>
    </body>
</html>

运行结果:

1.3. UMD

UMD:Universal Module Definition(通用模块规范)是由社区想出来的一种整合了CommonJS 和 AMD 两个模块定义规范的方法。

main.js:

export default "Hello World!";

bundle.js:

  • 如果在 cjs 环境下?
  • 如果在 amd 环境下?
  • 导出到全局;(注意 globalThis 的应用)
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
      (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.hello = factory());
}(this, (function () {
  'use strict';
  var main = "Hello World!";
  return main;
})));

1.4. 为什么 ES 模块比 CommonJS 更好?

ES modules are an official standard and the clear path forward for JavaScript code structure, whereas CommonJS modules are an idiosyncratic legacy format that served as a stopgap solution before ES modules had been proposed. ES modules allow static analysis that helps with optimizations like tree-shaking and scope-hoisting, and provide advanced features like circular references and live bindings.

1.5. 什么是 "tree-shaking"?

Tree-shaking, also known as "live code inclusion", is a process of eliminating code that is not actually used in a given project. It is a form of dead code elimination but can be much more efficient than other approaches with regard to output size. The name is derived from the abstract syntax tree of the modules (not the module graph). The algorithm first marks all relevant statements and then "shakes the syntax tree" to remove all dead code. It is similar in idea to the mark-and-sweep garbage collection algorithm. Even though this algorithm is not restricted to ES modules, they make it much more efficient as they allow us to treat all modules together as a big abstract syntax tree with shared bindings.

1.6. mjs 是什么?

On the Web, the file extension doesn’t really matter, as long as the file is served with the JavaScript MIME type text/javascript. The browser knows it’s a module because of the type attribute on the script element.

Still, we recommend using the .mjs extension for modules, for two reasons:

  1. During development, the .mjs extension makes it crystal clear to you and anyone else looking at your project that the file is a module as opposed to a classic script. (It’s not always possible to tell just by looking at the code.) As mentioned before, modules are treated differently than classic scripts, so the difference is hugely important!
  2. It ensures that your file is parsed as a module by runtimes such as Node.js and d8, and build tools such as Babel. While these environments and tools each have proprietary ways via configuration to interpret files with other extensions as modules, the .mjs extension is the cross-compatible way to ensure that files are treated as modules.

Note: To deploy .mjs on the web, your web server needs to be configured to serve files with this extension using the appropriate Content-Type: text/javascript header, as mentioned above. Additionally, you may want to configure your editor to treat .mjs files as .js files to get syntax highlighting. Most modern editors already do this by default.

1.7. unpkg 是什么?

  • unpkg is a fast, global content delivery network for everything on npm.
  • Use it to quickly and easily load any file from any package using a URL like:
    • unpkg.com/:package@:version/:file
      • 例:https://unpkg.com/jquery@3.5.1/dist/jquery.js

1.8. pkg.module 是什么?

pkg.module will point to a module that has ES2015 module syntax but otherwise only syntax features that the target environments support.

Typically, a library will be accompanied with a package.json file (this is mandatory if you're publishing on npm, for example). That file will often specify a pkg.main property - something like this:

{
  "name": "my-package",
  "version": "0.1.0",
  "main": "dist/my-package.js"
}

That instructs Browserify or Webpack or [insert module bundler here] to include the contents of dist/my-package.js – plus any dependencies it has – in your bundle when you call require('my-package') in your app or library.

But for ES2015-aware tools like Rollup, using the CommonJS (or Universal Module Definition) build isn't ideal, because we can't then take advantage of ES2015 module features. So assuming that you've written your package as ES2015 modules, you can generate an ES2015 module build alongside your CommonJS/UMD build:

{
  "name": "my-package",
  "version": "0.1.0",
  "main": "dist/my-package.umd.js",
  "module": "dist/my-package.esm.js"
}

Now we're cooking with gas - my-package continues to work for everyone using legacy module formats, but people using ES2015 module bundlers such as Rollup don't have to compromise. Everyone wins!

1.9. commonjs2 是什么?

CommonJs spec defines only exports. But module.exports is used by node.js and many other CommonJs implementations.

  • commonjs mean pure CommonJs
  • commonjs2 also includes the module.exports stuff.

2. 构建一个库

下面我们选几个主流打包工具

分别构建同一个库

看看它们各自有啥特点

2.1. 构建需求?

  • 库名:webj2ee-numbers
  • 非模块化环境下的访问名:webj2eeNumbers
  • 导入方式:
    • ES2015:
      • import * as webj2eeNumbers from 'webj2ee-numbers'; // ... webj2eeNumbers.wordToNum('Two');
    • CommonJS:
      • const webj2eeNumbers = require('webj2ee-numbers'); // ... webj2eeNumbers.wordToNum('Two');
    • AMD module require:
      • // 关联 lodash cdn requirejs.config({ "paths": { "lodash": "//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min" } }); require(['webj2ee-numbers'], function (webj2eeNumbers) { // ... const x= webj2eeNumbers.wordToNum('Two'); alert(x); });
    • script tag
      • <!doctype html> <html> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script> <script src="https://unpkg.com/webj2ee-numbers"></script> <script> // ... // Global variable const x= webj2eeNumbers.wordToNum('Five') console.log(x); </script> </html>
  • 外部依赖:
    • lodash 是外部依赖,不应打包到 webj2ee-numbers 中。
  • 源码结构:

2.2. 用 webpack 构建

webpack.config.js:

const path = require('path');
module.exports = {
    mode: 'development',
    entry: './src/index.js',
    devtool: "none",
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'webj2ee-numbers.js',
        library: 'webj2eeNumbers',
        libraryTarget: 'umd',
    },
    externals: {
        lodash: {
            commonjs: 'lodash',
            commonjs2: 'lodash',
            amd: 'lodash',
            root: '_',
        },
    },
};

dist/webj2ee-numbers.js:

2.3. 用 Rollup.js 构建?

rollup.config.js:

import json from 'rollup-plugin-json';

export default {
    input: 'src/index.js',
    output: {
      file: 'dist/webj2ee-numbers.js',
      format: 'umd',
      name: "webj2eeNumbers",
      globals: {
        lodash: '_'
      }
    },
    external: ['lodash'], // 将 lodash 视为外部模块
    plugins: [ json() ]
  };
  

dist/webj2ee-numbers.js:

2.4. 用 father-build 构建?

  • 基于 docz 的文档功能
  • 基于 rollup 和 babel 的组件打包功能
  • 支持 TypeScript
  • 支持 cjs、esm 和 umd 三种格式的打包
  • esm 支持生成 mjs,直接为浏览器使用
  • 支持用 babel 或 rollup 打包 cjs 和 esm
  • 支持多 entry
  • 支持 lerna
  • 支持 css 和 less,支持开启 css modules
  • 支持 test
  • 支持用 prettier 和 eslint 做 pre-commit 检查

.fatherrc.js:

  • external 可通过 dependencies 和 peerDependencies 的约定实现。
export default {
    entry: 'src/index.js',
    esm: "rollup",
    cjs: "rollup",
    umd: {
        name: "webj2eeNumbers",
        globals:{
            lodash: '_'
          }
    }
}

打包结果:

参考:

globalThis: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis UMD: https://github.com/umdjs/umd mjs: https://v8.dev/features/modules#mjs unpkg: https://unpkg.com/ pkg.module: https://github.com/rollup/rollup/wiki/pkg.module commonjs2: https://github.com/webpack/webpack/issues/1114 es模块化语法回顾: https://www.rollupjs.org/guide/en/#es-module-syntax webpack - Libraries https://webpack.js.org/guides/author-libraries/ https://webpack.js.org/configuration/externals/ Rollup.js: https://www.rollupjs.org/guide/en/#faqs https://www.rollupjs.com/ father-build: https://www.npmjs.com/package/father-build https://github.com/umijs/father 利用 umi-library(father) 做组件打包: https://www.bilibili.com/video/av47853431