神族九帝's blog 神族九帝's blog
首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)

神族九帝,永不言弃

首页
  • 神卡套餐 (opens new window)
  • 神族九帝 (opens new window)
  • 网盘资源 (opens new window)
  • 今日热点 (opens new window)
  • 在线PS (opens new window)
  • IT工具 (opens new window)
  • FC游戏 (opens new window)
  • 在线壁纸 (opens new window)
  • 面试突击
  • 复习指导
  • HTML
  • CSS
  • JavaScript
  • 设计模式
  • 浏览器
  • 手写系列
  • Vue
  • Webpack
  • Http
  • 前端优化
  • 项目
  • 面试真题
  • 算法
  • 精选文章
  • 八股文
  • 前端工程化
  • 工作笔记
  • 前端基础建设与架构 30 讲
  • vue2源码学习
  • 剖析vuejs内部运行机制
  • TypeScript 入门实战笔记
  • vue3源码学习
  • 2周刷完100道前端优质面试真题
  • 思维导图
  • npm发包
  • 重学node
  • 前端性能优化方法与实战
  • webpack原理与实战
  • webGl
  • 前端优化
  • Web3
  • React
  • 更多
  • 未来要做的事
  • Stirling-PDF
  • ComfyUI
  • 宝塔面板+青龙面板
  • 安卓手机当服务器使用
  • 京东自动评价代码
  • 搭建x-ui免流服务器(已失效)
  • 海外联盟
  • 好玩的docker
  • 收藏夹
  • 更多
GitHub (opens new window)
  • 工作笔记

  • 前端基础建设与架构 30 讲

    • 开篇词 像架构师一样思考,突破技术成长瓶颈
    • npm 安装机制及企业级部署私服原理
    • Yarn 的安装理念及如何破解依赖管理困境
    • CI 环境上的 npm 优化及更多工程化问题解析
    • 横向对比主流构建工具,了解构建工具的设计考量
    • Vite 实现:从源码分析出发,构建 bundlele 开发工程
    • core-j 及垫片理念:设计一个“最完美”的 Polyfill 方案
    • 梳理混乱的 Babel,不再被编译报错困扰
    • 探索前端工具链生态,制定一个统一标准化 babel-preet
    • 从实战出发,从 0 到 1 构建一个符合标准的公共库
      • 代码拆分和按需加载:缩减 bundle ize,把性能做到极致
      • Tree Shaking:移除 JavaScript 上下文中的未引用代码
      • 如何理解 AST 实现和编译原理?
      • 工程化思维处理方案:如何实现应用主题切换功能?
      • 解析 Webpack 源码,实现自己的构建工具
      • 从编译到运行,跨端解析小程序多端方案
      • 原生跨平台技术:移动端跨平台到 Flutter 的技术变革
      • 学习 axio:封装一个结构清晰的 Fetch 库
      • 对比 Koa 和 Redux:分析前端中的中间件理念
      • 如何理解软件开发灵活性和高定制性?
      • 如何理解前端中面向对象的思想?
      • 如何利用 JavaScript 实现经典数据结构?
      • 剖析前端中的数据结构应用场景
      • npm cript:打造一体化的构建和部署流程
      • 自动化代码检查:剖析 Lint 工具和工程化接入&优化方案
      • 如何设计一个前端 + 移动端离线包方案?
      • 如何设计一个“万能”项目脚手架?
      • 同构渲染架构:实现一个 SSR 应用
      • 设计性能守卫系统:完善 CICD 流程
      • 实践打造网关:改造企业 BFF 方案
      • 实现高可用:使用 Puppeteer 生成性能最优的海报系统
      • 结束语 再谈项目的基建和架构,个人的价值和方向
    • vue2源码学习

    • 剖析vuejs内部运行机制

    • TypeScript 入门实战笔记

    • vue3源码学习

    • 2周刷完100道前端优质面试真题

    • 思维导图

    • npm发包

    • 重学node

    • 前端性能优化方法与实战

    • webpack原理与实战

    • webGl

    • 前端优化

    • Web3

    • React

    • 更多

    • 笔记
    • 前端基础建设与架构 30 讲
    wu529778790
    2024-04-07

    从实战出发,从 0 到 1 构建一个符合标准的公共库

    上一讲我们从 Babel 编译预设的角度理清了前端生态中的公共库和应用的丝缕关联,这一讲我们就从实战出发,动手剖析一个公共库从设计到完成的过程。

    (源码出处:Creating a simple npm library to use in and out of the browser)

    实战打造一个公共库

    下面我们从实战出发,从 0 到 1 构建一个符合标准的公共库。我们的目标是,借助 Public APIs,通过网络请求获取 dogs/cats/goats 三种动物的随机图像,并进行展示。更重要的是,将整个逻辑过程抽象成可以在浏览器端和 Node.js 端复用的 npm 包,编译构建使用 Webpack 和 Babel。

    首先创建文件:

    $ mkdir animal-api
    $ cd animal-api
    $ npm init
    

    并通过 npm init 初始化一个 package.json 文件:

    {
      "name": "animal-api",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC"
    }
    

    编写index.js代码逻辑非常简单,如下:

    import axios from 'axios';
    const getCat = () => {
        // 发送请求
        return axios.get('https://aws.random.cat/meow').then((response) => {
            const imageSrc = response.data.file
            const text = 'CAT'
            return {imageSrc, text}
        })
    }
    const getDog = () => {
        return axios.get('https://random.dog/woof.json').then((response) => {
            const imageSrc = response.data.url
            const text = 'DOG'
            return {imageSrc, text}
        })
    }
    const getGoat = () => {
        const imageSrc = 'http://placegoat.com/200'
        const text = 'GOAT'
        return Promise.resolve({imageSrc, text})
    }
    export default {
        getDog,
        getCat,
        getGoat
    }
    

    我们通过三个接口:

    • https://aws.random.cat/meow

    • https://random.dog/woof.json

    • http://placegoat.com/200

    封装了三个获取图片地址的函数:

    • getDog()

    • getCat()

    • getGoat()

    源码通过 ESM 的方式提供对外接口,请你注意这里的模块化方式,这是一个公共库设计的关键点之一,后文会更详细解析。

    对公共库来说,质量保证至关重要。我们使用 Jest 来进行 animal-api 这个公共库的单元测试。Jest 作为 devDependecies 被安装,代码如下:

    npm install --save-dev jest
    

    编写测试脚本animal-api/spec/index.spec.js:

    import AnimalApi from '../index'
    describe('animal-api', () => {
        it('gets dogs', () => {
            return AnimalApi.getDog()
                .then((animal) => {
                    expect(animal.imageSrc).not.toBeUndefined()
                    expect(animal.text).toEqual('DOG')
                })
       })
    })
    

    改写 package.json 中 test script 为 "test": "jest",我们通过运行npm run test来测试。

    这时候会得到报错:SyntaxError: Unexpected identifier,如下图所示:

    Drawing 0.png

    不要慌,这是因为 Jest 并不“认识”import 这样的关键字。Jest 运行在 Node.js 环境中,大部分 Node.js 版本(v10 以下)运行时并不支持 ESM,为了可以使用 ESM 方式编写测试脚本,我们需要安装 babel-jest 和 Babel 相关依赖到开发环境中:

    npm install --save-dev babel-jest @babel/core @babel/preset-env
    

    同时创建babel.config.js,内容如下:

    module.exports = {
      presets: [
        [
          '@babel/preset-env',
          {
            targets: {
              node: 'current',
            },
          },
        ],
      ],
    };
    

    注意上述代码,我们将 @babel/preset-env 的 targets.node 属性设置为当前环境 current。再次执行npm run test,得到报错如下:Cannot find module 'axios' from 'index.js'。

    Drawing 1.png

    原因看报错信息即可得到,我们需要安装 axios。注意:axios 应该作为生产依赖被安装:

    npm install --save axios
    

    现在,我们的测试脚本就可以正常运行了。如下图:

    Drawing 2.png

    当然,这只是给公共库接入测试,“万里长征”才开始第一步。接下来我们按照各种场景进行更多探索。

    打造公共库,支持 script 标签引入

    在大部分不支持 import 语法特性的浏览器中,为了让我们的脚本直接在浏览器中使用 script 标签引入代码,首先我们需要将已有公共库脚本编译为 UMD 方式。类似上面使用 babel-jest 将测试脚本编译降级为当前 Node.js 版本支持的代码,我们还是需要 Babel 进行降级。

    注意这次不同之处在于:这里的降级需要输出代码内容到一个 output 目录中,浏览器即可直接引入该 output 目录中的编译后资源。我们使用@babel/plugin-transform-modules-umd来完成对代码的降级编译:

    $ npm install --save-dev @babel/plugin-transform-modules-umd @babel/core @babel/cli
    

    同时在 package.json 中加入相关 script 内容:"build": "babel index.js -d lib",执行npm run build,得到产出(如下图):

    Drawing 3.png

    我们在浏览器中验证产出:

    <script src="./lib/index.js"></script>
    <script>
        AnimalApi.getDog().then(function(animal) {
            document.querySelector('#imageSrc').textContent = animal.imageSrc
            document.querySelector('#text').textContent = animal.text
        })
    </script>
    

    结果出现了报错:

    index.html:11 Uncaught ReferenceError: AnimalApi is not defined
        at index.html:11
    

    并没有找到 AnimalApi 这个对象,重新翻看编译产出源码:

    "use strict";
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.default = void 0;
    // 引入 axios
    var _axios = _interopRequireDefault(require("axios"));
    //  兼容 default 导出
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
    // 原 getCat 方法
    const getCat = () => {
      return _axios.default.get('https://aws.random.cat/meow').then(response => {
        const imageSrc = response.data.file;
        const text = 'CAT';
        return {
          imageSrc,
          text
        };
      });
    };
    // 原 getDog 方法
    const getDog = () => {
      return _axios.default.get('https://random.dog/woof.json').then(response => {
        const imageSrc = response.data.url;
        const text = 'DOG';
        return {
          imageSrc,
          text
        };
      });
    };
    // 原 getGoat 方法
    const getGoat = () => {
      const imageSrc = 'http://placegoat.com/200';
      const text = 'GOAT';
      return Promise.resolve({
        imageSrc,
        text
      });
    };
    // 默认导出对象
    var _default = {
      getDog,
      getCat,
      getGoat
    };
    exports.default = _default;
    

    发现出现报错是因为 Babel 的编译产出如果要支持全局命名(AnimalApi)空间,需要添加以下配置:

      plugins: [
          ["@babel/plugin-transform-modules-umd", {
          exactGlobals: true,
          globals: {
            index: 'AnimalApi'
          }
        }]
      ],
    

    调整后再运行编译,得到源码:

    // umd 导出格式
    (function (global, factory) {
      // 兼容 amd 方式
      if (typeof define === "function" && define.amd) {
        define(["exports", "axios"], factory);
      } else if (typeof exports !== "undefined") {
        factory(exports, require("axios"));
      } else {
        var mod = {
          exports: {}
        };
        factory(mod.exports, global.axios);
        // 挂载 AnimalApi 对象
        global.AnimalApi = mod.exports;
      }
    })(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (_exports, _axios) {
      "use strict";
      Object.defineProperty(_exports, "__esModule", {
        value: true
      });
      _exports.default = void 0;
      _axios = _interopRequireDefault(_axios);
      // 兼容 default 导出
      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
      const getCat = () => {
        return _axios.default.get('https://aws.random.cat/meow').then(response => {
          const imageSrc = response.data.file;
          const text = 'CAT';
          return {
            imageSrc,
            text
          };
        });
      };
      const getDog = () => {
        // ... 省略
      };
      const getGoat = () => {
        // ... 省略
      };
      var _default = {
        getDog,
        getCat,
        getGoat
      };
      _exports.default = _default;
    });
    

    这时,编译源码产出内容改为了由一个 IIFE 形式实现的命名空间。同时观察源码:

    global.AnimalApi = mod.exports;
    ...
    var _default = {
        getDog,
        getCat,
        getGoat
      };
      _exports.default = _default;
    

    为了兼容 ESM 特性,导出内容全部挂在了 default 属性中(可以通过 libraryExport 属性来切换),我们的引用方式需要改为:

    AnimalApi.default.getDog().then(function(animal) {
        ...
    })
    

    解决了以上所有问题,看似大功告成了,但是工程的设计没有这么简单。事实上,在源码中,我们没有使用引入并编译 index.js 所需要的依赖,比如 axios 并没有被引入处理。正确的方式应该是把公共库需要的依赖,一并按照依赖关系进行打包和引入。

    为了解决上面这个问题,此时需要引入 Webpack:

    npm install --save-dev webpack webpack-cli
    

    同时添加webpack.config.js,内容为:

    const path = require('path');
    module.exports = {
      entry: './index.js',
      output: {
        path: path.resolve(__dirname, 'lib'),
        filename: 'animal-api.js',
        library: 'AnimalApi',
        libraryTarget: 'var'
      },
    };
    

    我们设置入口为./index.js,构建产出为./lib/animal-api.js,同时通过设置 library 和 libraryTarget 将 AnimalApi 作为公共库对外暴露的命名空间。修改package.json中的 build script 为"build": "webpack",执行npm run build,得到产出,如下图:

    Drawing 4.png

    至此,我们终于构造出了能够在浏览器中通过 script 标签引入的公共库。当然,一个现代化的公共库还需要支持更多场景,请继续阅读。

    打造公共库,支持 Node.js 环境

    现在已经完成了公共库的浏览器端支持,下面我们要集中精力适配一下 Node.js 环境了。

    首先编写一个node.test.js文件,进行 Node.js 环境的验证:

    const AnimalApi = require('./index.js')
    AnimalApi.getCat().then(animal => {
        console.log(animal)
    })
    

    这个文件的意义在于测试公共库是否能在 Node.js 环境下使用。执行node node-test.js,不出意料得到报错,如下图:

    Drawing 5.png

    这个错误我们并不陌生,在 Node.js 环境中,我们不能通过 require 来引入一个通过 ESM 编写的模块化文件。上面的操作中,我们通过 Webpack 编译出来了符合 UMD 规范的代码,尝试修改node.test.js文件为:

    const AnimalApi = require('./lib/index').default
    AnimalApi.getCat().then((animal) => {
        console.log(animal)
    })
    

    如上代码,我们按照require('./lib/index').default的方式引用,就可以愉快地在 Node.js 环境中运行了。

    事实上,依赖上一步的构建产出,我们只需要按照正确的引用路径,就可以轻松地支持 Node.js 环境了。是不是有些恍恍惚惚:“基本什么都没做,这就搞定了”,下面,我们从代码原理上阐述说明。

    符合 UMD 规范的代码,形如:

    (function (root, factory) {
        if (typeof define === 'function' && define.amd) {
            // AMD. Register as an anonymous module.
            define(['b'], factory);
        } else if (typeof module === 'object' && module.exports) {
            // Node.
            module.exports = factory(require('b'));
        } else {
            // Browser globals (root is window)
            root.returnExports = factory(root.b);
        }
    }(typeof self !== 'undefined' ? self : this, function (b) {
        // Use b in some fashion.
        // Just return a value to define the module export.
        // This example returns an object, but the module
        // can return a function as the exported value.
        return {};
    }));
    

    如上结构,通过 if...else 判断是否根据环境加载代码。我们的编译产出类似上面 UMD 格式,因此是天然支持浏览器和 Node.js 环境的。

    但是这样的设计将 Node.js 和浏览器环境融合在了一个 bundle 当中,并不优雅,也不利于使用方优化。另外一个常见的做法是将公共库按环境区分,分别产出两个 bundle,分别支持 Node.js 和浏览器环境。如下图架构:

    Drawing 6.png

    公共库支持浏览器/Node.js 环境方式示意图

    当然,如果编译和产出为两种不同环境的资源,还得需要设置 package.json 中的相关字段。事实上,如果一个 npm 需要在不同环境下加载 npm 包不同的入口文件,就会牵扯到main字段、module以及browser字段。简单来说:

    • main定义了npm包的入口文件,Browser 环境和 Node 环境均可使用;

    • module定义npm包的 ESM 规范的入口文件,Browser 环境和 Node 环境均可使用;

    • browser定义npm包在 Browser 环境下的入口文件。

    而这三个字段也需要区分优先级,打包工具对于不同环境适配不同入口的字段在选择上还是要以实际情况为准。经我测试后,在目前状态,Webpack 在 Web 浏览器环境配置下,优先选择:browser > module > main,在 Node.js 环境下 module > main。

    从开源库总结生态设计

    最后一部分,我们针对一个真正的公共库,来总结一下编译适配不同环境的“公共库最佳实践”。simple-date-format 可以将 Date 类型转换为标准定义格式的字符串类型,它支持了多种环境:

    import SimpleDateFormat from "@riversun/simple-date-format";
    const SimpleDateFormat = require('@riversun/simple-date-format');
    <script src="https://cdn.jsdelivr.net/npm/@riversun/simple-date-format@1.1.2/lib/simple-date-format.js"></script>
    

    使用方式也很简单:

    const date = new Date('2018/07/17 12:08:56');
    const sdf = new SimpleDateFormat();
    console.log(sdf.formatWith("yyyy-MM-dd'T'HH:mm:ssXXX", date));//to be "2018-07-17T12:08:56+09:00"
    

    我们看这个公共库的相关设计,源码如下:

    // 入口配置
    entry: {
      'simple-date-format': ['./src/simple-date-format.js'],
    },
    // 产出配置
    output: {
      path: path.join(__dirname, 'lib'),
      publicPath: '/',
      // 根据环境产出不同的文件名
      filename: argv.mode === 'production' ? `[name].js` : `[name].js`,  //`[name].min.js`
      library: 'SimpleDateFormat',
      libraryExport: 'default',
      // umd 模块化方式
      libraryTarget: 'umd',
      globalObject: 'this',//for both browser and node.js
      umdNamedDefine: true,
      // 在和 output.library 和 output.libraryTarget 一起使用时,auxiliaryComment 选项允许用户向导出文件中插入注释
      auxiliaryComment: {
        root: 'for Root',
        commonjs: 'for CommonJS environment',
        commonjs2: 'for CommonJS2 environment',
        amd: 'for AMD environment'
      }
    },
    

    设计方式与前文类似,因为这个库的目标就是:作为一个函数 helper 库,同时支持浏览器和 Node.js 环境。它采取了比较“偷懒”的方式,使用了 UMD 规范来输出代码。

    我们再看另一个例子,在 Lodash 的构建脚本中,分为了:

    "build": "npm run build:main && npm run build:fp",
    "build:fp": "node lib/fp/build-dist.js",
    "build:fp-modules": "node lib/fp/build-modules.js",
    "build:main": "node lib/main/build-dist.js",
    "build:main-modules": "node lib/main/build-modules.js",
    

    其中主命令为 build,同时按照编译所需,提供:ES 版本、FP 版本等(build:fp/build:fp-modules/build:main/build:main-modules)。官方甚至提供了 lodash-cli 支持开发者自定义构建,更多相关内容可以参考 Custom Builds。

    我们在构建环节“颇费笔墨”,目的是让你理解前端生态天生“混乱”,不统一的运行环境使得公共库的架构,尤其是相关的构建设计更加复杂。更多构建相关内容,我们会在后续章节继续讨论,这里先到此为止。

    总结

    这两节课我们从公共库的设计和使用方接入两个方面进行了梳理。当前前端生态多种规范并行、多类环境共存,因此使得“启用或设计一个公共库”并不简单,单纯的 'npm install' 后,才是一系列工程化问题的开始。

    与此同时,开发者经常疲于业务开发,对于编译和构建,以及公共库设计和前端生态的理解往往得过且过,但这些内容正是前端基础设施道路上的重要一环,也是开发者通往前端架构师的必经之路。建议你将本节知识融入自己手上的真实项目中,刨根问底,相信你一定会有更多收获!

    Lark20210108-153014.png

    最后,如果本节内容你难以一步到位地理解消化,请不要灰心,我们会在后续章节中不断巩固梳理。我们下一讲再见!


    # 精选评论

    # **骏:

    作为一个库不应该把 axios 打包进去,因为业务方很可能也会使用 axios,这样会造成打包两次。正确的做法是将 axios 加入 peerDependencies,然后 webpack 中加入 external 排除掉

    # *俊:

    赞👍🏻

    # *野:

    照着做了一遍,引入webpack之后才能正确跑起来,明白了引入第三方js模块,比如axios的时候使用webpack工具的作用

    # **棉:

    受益匪浅~求问老师一般在工作上也很难有机会去构建大型的基础建设项目,这一部分经验的缺失平时该如何弥补,确实自己本身写demo的时候也很难会遇到那么多问题~平时应该如何锻炼呢

    #     讲师回复:

        可以再看看开篇词吧,我在开篇词里有说过这个问题~

    # **4550:

    老师,这句话怎么理解:“为了兼容 ESM 特性,导出内容全部挂在了 default 属性”

    #     讲师回复:

        这是因为 ESM 和 CommonJS 的规范,关于 default 设计的不同

    编辑 (opens new window)
    上次更新: 2025/03/17, 12:21:00
    探索前端工具链生态,制定一个统一标准化 babel-preet
    代码拆分和按需加载:缩减 bundle ize,把性能做到极致

    ← 探索前端工具链生态,制定一个统一标准化 babel-preet 代码拆分和按需加载:缩减 bundle ize,把性能做到极致→

    最近更新
    01
    Code Review
    10-14
    02
    ComfyUI
    10-11
    03
    vscode插件开发
    08-24
    更多文章>
    Power by vuepress | Copyright © 2015-2025 神族九帝
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×