一、一段失败的面试

让咱们先来看一段Element-UI运用者典型的面试问答

面:什么是组件的按需引进?

回:按需引进便是在项目中只引进并打包自己所需求运用到的组件,将其它无需运用的组件代码除去出去从而缩小项目体积,提高页面加载速度。(简略简略)

面:那怎样完成组件按需引进呢?

回:貌似是有一个babel装备,balabala。。。(有一点心慌,细节记不清了)

面:什么原理呢?

回:嗯。。。(没看过啊)

面:知道Tree-Shaking么?为什么不直接用Tree-Shaking来做呢?

回:嗯,知道是知道。。。(对啊,为什么不必呢?)

面:那你对Monorepo有了解么?

回: 听过。。。(也仅仅听过)

面: 感谢你参与今天的面试,再会。

回: 再会。

咱们能够在阅览文章之前先自行答复一下上面所说到的问题,看看自己知道多少。假如发现自己并不比上面的面试者懂得更多的话,那么你需求深化研究一下组件按需引进这个话题了。

能够看到在运用组件库进行前端页面开发的进程中,组件按需引进是每个前端程序员在开发进程中都或多或少了解过的功用。但与此一起,它又是一个许多同学没有深化研究,只停留在复制粘贴组件库官方文档所供给的方法到自己项目中运用的功用。这篇文章将从组件库按需引进方法的介绍入手,进一步加深读者对前端工程化中遇到的问题和概念的了解。

**留意:文章中的代码写法仅为解说示例运用,并非为真实组件库的开发设计。示例组件库并未上传NPM,经过本地npm link来模仿装置。别的,相关代码现已悉数上传至github.com/Owen9511/On…

二、没有按需引进的国际

那是一个混沌的时代,大地上熔岩遍地,天空中电闪雷鸣,国际上只存在WEB程序员。在那个前端程序员还处在襁褓之中的时代,JS还仅仅玩具言语,用户页面的交互也非常有限。与庞大的JAVA后端代码比较,没有人能够想到前端代码的体积还需求被优化减缩。

停一下。。。这儿咱们不再深化研究过去的历史,可是咱们能够经过现代的手法来展示一下没有按需引进的组件库是怎样作业的(当然现在大部分前端程序员在运用组件库时也是这样做的)。

虚拟一个最最简略的组件库

my-compo-test这个组件库由两个只返回一段字符串的组件构成。

// mycompotest/lib/components/Alert/index.js 
import './style/index.css'
export default function alert(){
    return 'alert'
}
// mycompotest/lib/components/Button/index.js 
import './style/index.css'
export default function button(){
    return 'button'
}

作为组件库的作者,咱们决议运用Webpack来对其进行打包构建,一起为了打包出的代码能够以多种方法被不同的运用者运用,咱们采取了umd输出的方法。

// mycompotest/webpack.config.js
module.exports = {
    mode: 'none',
    entry: './lib/index.js',
    output: {
        filename: 'index.js',
        path: __dirname + '/dist',
        library: {
            name: 'myCompoTest',
          	// 留意:这儿为了使组件库能被不同引进方法的运用者运用,咱们输出了UMD格局
            type: 'umd'
        },
    },
    module: {
        rules: [{
            test: /.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        },{
            test: /.js$/,
          	// 留意:同上,为了通用性,咱们经过babel转译了咱们的语法,babel装备如下
            use: 'babel-loader'
        },]
    }
}
=========================================================================
// mycompotest/.babelrc
{
    "presets": [
      "@babel/preset-env"
    ]
}

之后,咱们打包这个组件库而且装备package.json中的main字段将打包后的文件暴露给组件库运用者。

// mycompotest/package.json
{
  "name": "my-compo-test",
  "version": "1.0.0",
  "description": "",
  "main": "dist/index.js",
  // 省掉之后的....
}

怎样引证组件库

从组件运用者的视点,咱们需求在工程中引进my-compo-test中的Button组件。

// test/index.js
import {Button} from 'my-compo-test'
console.log(Button)

之后咱们装备webpack以及babel并对这个项目进行打包。

// test/webpack.config.js
module.exports = {
    mode: 'none',
    entry: './index.js',
    output: {
        filename: 'index.js',
        path: __dirname + '/dist',
    },
    module: {
        rules: [{
            test: /.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        },{
            test: /.js$/,
            use: 'babel-loader'
        },]
    }
}
// test/.babelrc
{
    "presets": [
      "@babel/preset-env"
    ]
}

调查打包成果,在dist目录下的index.js文件中能够看到打包出来的代码不只包含了Button按钮组件及其相关款式,一起也将Alert组件及其相关款式一起打包进去了。

// test/dist/index.js
/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// =======省掉中心内容=========
var css_248z$1 = ".button{n    background-color: white;n}";
styleInject(css_248z$1);
function button() {
  return 'button';
}
var css_248z = ".alert{n    background-color: black;n}";
styleInject(css_248z);
function alert() {
  return 'alert';
}
var index = {
  Button: button,
  Alert: alert
};
// =======省掉之后内容=========

定论

这样整体引进的做法在咱们这个虚拟的小组件库中当然能够。可是一旦组件库变得庞大起来,这种只为了运用组件库中的某一个组件从而打包悉数组件库代码的行为就开端变得不行接受了起来。从这时开端,聪明的程序员们为了减缩代码体积提出了组件按需引进的概念,也便是下面咱们所要要点解说的内容。

三. 最原始的方法 —— 手动引进

先停下来代入那个还没有任何辅助手法的时代考虑一下,假如让你完成上面组件库的按需引进,你会怎样做?

没错,榜首反应便是我只引证我需求运用的文件而不引证其它组件不就能够了么?

怎样完成手动引进

找到my-compo-test组件库的代码,能够看到Button组件的目录为my-compo-test/lib/components/Button

// test/index.js
import Button from 'my-compo-test/lib/components/Button'
console.log(Button)

咱们再从头运转npm run build打包看一下成果,能够看到Alert组件相关的代码及款式现已被移除了。

// test/dist/index.js
/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ([
// =======省掉中心内容=========
function button() {
  return 'button';
}
// =======省掉中心内容=========
var ___CSS_LOADER_EXPORT___ = _test_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_test_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".button{n    background-color: white;n}", ""]);
// Exports
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
// =======省掉之后内容=========

定论

经过手动引进需求的组件文件,咱们开始完成了组件的按需引进。可是,从运用者的视点出发,这样的方法需求深化了解组件库代码,如组件放在哪里、应该引证哪些文件都需求被考虑,十分不便。因而,现在大部分组件库现已抛弃了这种方法。

四. 进阶模式 —— Babel-plugin-import

已然咱们靠手动引进的方法能够完成组件的按需引进,那么只需求再往前走一步,让程序主动协助咱们把第二节中的代码转换成第三节中的代码不就能够了嘛。那怎样完成主动转换呢?假如你对前端工程化比较了解的话,那么你一定会想到Babel。通常咱们运用Babel是为了将ES6的一些语法转换从而让低版别浏览器也能运转咱们的代码,Babel之所以能够做到这点是经过剖析代码得到AST(Abstract Syntax Tree)并对其进行修改。换个思路,咱们的import {Button} from 'my-compo-test'语句同样能够经过Babel的解析和修改终究成为import Button from 'my-compo-test/lib/components/Button'的姿态。没错,这便是Element-UI、iView等组件库完成组件按需引进的方法。

当然,你能够自己写一个Babel插件来完成这样的转换,不过没有必要再造一个轮子了——Babel-plugin-import这个插件现已能够完成咱们大部分需求了。

怎样运用Babel-plugin-import

从头调整组件引进代码为第二节中没有特别处理过的。

// test/index.js
import {Button} from 'my-compo-test'
console.log(Button)

在.babelrc中装备该组件的运用。

// test/.babelrc
{
    "presets": [
      "@babel/preset-env"
    ],
    // 留意:凭借babel-plugin-import引进
    "plugins": [["import",{
      	// 组件库名称
        "libraryName": "my-compo-test",
      	// 组件方位
        "libraryDirectory": "lib/components",
      	// 组件款式方位
        "style": "style/index.css"
    }]]
}

再次打包调查成果,能够看到同样Alert组件相关的代码及款式现已被移除了。

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ([
/* 0 */,
/* 1 */
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (/* binding */ button)
/* harmony export */ });
/* harmony import */ var _style_index_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
function button() {
  return 'button';
}
// =======省掉中心内容=========
var ___CSS_LOADER_EXPORT___ = _test_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_test_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));
// Module
___CSS_LOADER_EXPORT___.push([module.id, ".button{n    background-color: white;n}", ""]);
// Exports
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
// =======省掉之后内容=========

定论

Babel-plugin-import凭借Babel解析修改ATS的才能完成了一种成熟的组件按需引进方法,现在许多的组件库都是依托这种方法进行了完成,例如Element-UI等。这种方法的长处是不需求库运用者了解原理,只需依照文档说明装备即可。缺陷是运用者仍旧需求为了按需引进来增加或许本来无需运用的Babel及其相关装备(这点也是现在仍然有大量程序员不运用组件按需引进功用的原因,不是一切人都想触碰Babel装备的,特别当他还不够了解Babel机制的时分)。

五. Tree-Shaking能够完成吗?

跳出之前的处理计划,让咱们来考虑一下还有别的计划能够完成么?

用过Lodash等函数库的同学或许知道,在许多时分,咱们引进Lodash仅仅为了运用其间的一两个函数,因而咱们需求经过Tree-Shaking来摇掉其他不必要的函数(当然许多程序员也是不做这一步的,谢天谢地,高速的网络基建拯救了你们的项目)。

什么是Tree-Shaking

这儿首要简略介绍一下什么是Tree-Shaking以及它大约的原理,以便于还没有深化了解过这个概念的同学树立一个开始的映像。

其实早在编译原理中就有DCE(dead code eliminnation)的概念,作用是消除不行能履行的代码,它的作业原理是运用修改器判别出某些代码是不行能履行的,然后清除。所谓Tree-shaking便是“摇”的意思,也是为了消除项目中不必要的JS模块或JS模块内代码,从而减缩项目的体积。能够说Tree-Shaking是DCE的一种完成。

需求理解的是Tree-shaking是依靠ESM这种模块引进方法的:

ES6模块依靠联系是确认的,和运转时的状态无关,能够进行可靠的静态剖析,从而构成了tree-shaking的根底。所谓静态剖析便是不履行代码,从字面量上对代码进行剖析,能够想一下在commonjs的模块化中咱们能够动态require一个模块,只有履行后才知道引证的什么模块,这个就不能经过静态剖析去做优化。总之便是一句话——代码是否有用只有在编译时剖析,才不会影响运转时的状态。

Rollup最早完成了Tree-Shaking,后来Webpack2也完成了这个功用。网上现已有大量的文章对其进行了解说,这儿咱们就不再对其进行深化研究了。

说起来简略做起来难

说起来简略做起来难是我对Tree-Shaking功用的看法。尽管咱们只需求在webpack中装备几个相关的参数就能够敞开Tree-Shaking的功用,可是终究无用的代码是否能被删去去却彻底是一个难以掌控的事情。主要难点在于以下几个方面:

  1. Webpack不支撑ESM输出

    上面说过Tree-shaking是依靠ESM模块引进机制才干完成的,可是很遗憾,检查Webpack文档能够发现,操控输出的字段output.library.type并不支撑ESM模块的导出。不过从webpack5文档来看,未来是有期望支撑的。

    从组件按需引进深化前端打包构建

    因而,现在一切只经过Webpack打包的库就直接告别了Tree-Shaking。

  2. 代码副作用难以掌控

    Tree-Shaking更费事的一点是它只能删去没有副作用的无用代码,假如你不小心在代码中引进了副作用,那么同样你的这段代码也告别了Tree-Shaking。

    关于什么是代码副作用,咱们这儿有必要说明一下。下面给出了一个比如:

    // deep会被保存因为String.prototype这个全局变量依靠了deep
    let deep = 'old';
    // count会被删去,因为导出的函数以及全局变量中并没有运用count
    let count = 0;
    function withSideEffect() {
      // 有副作用的函数,部分被保存
      count ++;
      String.prototype.addOneMethod = () => {
        return deep;
      };
      window.newProp = "new";
    }
    function withoutSideEffect() {
      // 没有副作用的函数,被悉数删去
      count ++;
      deep = 'new';
      return count;
    }
    withoutSideEffect();
    withSideEffect();
    count++;
    export default function() {
    }
    

    Tree-Shaking打包后:

    [
        function (e, t, n) {
            "use strict";
            n.r(t);
            let o = "old";
            o = "new", String.prototype.addOneMethod = () => o, window.newProp = "new";
            (async() => {
                console.log(">>>in index.js")
            })()
        }
    ]
    

    能够看到有副作用的代码有或许会对import该模块之后的代码运转发生影响(比如import该文件后调用''.addOneMethod()),而影响是否发生不能够经过静态剖析确认,只能在运转时知道,因而Tree-Shaking不能删去这段代码。

    当然,有的同学或许会说,我的组件代码并不存在副作用啊,比如许多写React的同学或许会编写一个类似于以下这样的一个Class组件。

    export class Person {
      constructor ({ name, age, sex }) {
        this.className = 'Person'
        this.name = name
        this.age = age
        this.sex = sex
      }
      getName () {
        return this.name
      }
      //....
    }
    

    看起来这段代码彻底运转在一个类之中,没有对全局变量发生任何的影响,可是当你打包完组件库引进的时分会发现它仍然不会被Tree-Shaking删去去。这其实是因为咱们为了兼容ES5而引进了Babel,Babel会将ES6的Class翻译成ES5也能了解的prototype方法,从而引进了额定的副作用。

    能够看一下Babel编译后的成果:

    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
    var _createClass = function() {
      function defineProperties(target, props) {
        for (var i = 0; i < props.length; i++) {
          var descriptor = props[i];
          descriptor.enumerable = descriptor.enumerable || !1, descriptor.configurable = !0,
      		//以下Object.defineProperty发生了副作用,这个函数只有在运转时才知道target是什么(target有或许是window),无法静态				//剖析其是否有用
          "value" in descriptor && (descriptor.writable = !0), Object.defineProperty(target, descriptor.key, descriptor);
        }
      }
      return function(Constructor, protoProps, staticProps) {
        return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
        Constructor;
      };
    }()
    var Person = function () {
      function Person(_ref) {
        var name = _ref.name, age = _ref.age, sex = _ref.sex;
        _classCallCheck(this, Person);
        this.className = 'Person';
        this.name = name;
        this.age = age;
        this.sex = sex;
      }
      //以下函数发生了副作用
      _createClass(Person, [{
        key: 'getName',
        value: function getName() {
          return this.name;
        }
      }]);
      return Person;
    }();
    

    当然,这其实是能够经过设置babel装备"loose": false来防止(blog.csdn.net/qiwoo_weekl… 感兴趣的同学能够阅览这篇文章),可是这足以说明代码副作用是多难以掌控了。

没方法用Tree-Shaking了吗

当然不是!咱们假如能够防止上面说到的问题不就能够从头回到Tree-Shaking了嘛。

  1. 运用支撑ESM模块导出的Rollup进行打包。
  2. 抛弃或约束Babel的运用,毕竟能导入ESM代码的项目应该是支撑ES6语法的。
  3. 关于需求运用非ESM模式引进或不支撑ES6模块引进的项目,咱们继续采用上述的webpack打包方法。

咱们在组件库的打包中引进rollup的装备文件如下:

// mycompotest/rollup.config.js
import postcss from 'rollup-plugin-postcss';
export default {
    input: 'lib/index.js',
    output: {
      file: 'es/index.js',
      format: 'esm'
    },
    plugins: [
        postcss()
    ]
}

修改咱们的package.json文件使得打包后的ESM模块暴露出去。

// mycompotest/package.json
{
  "name": "my-compo-test",
  "version": "1.0.0",
  "description": "",
  //留意: commonjs,amd,cmd方法引进咱们的包时会将main中装备的文件作为进口
  "main": "dist/index.js",
  //留意: import方法引进咱们的包时会将module中装备的文件作为进口
  "module": "es/index.js",
  //留意: 需求声明sideEffects字段,表明咱们的模块是没有副作用的。sideEffects不是npm的规范字段,它是供webpack读取以了解能否运用tree-shaking的
  "sideEffects": false,
  // ....
}

在运用的时分,咱们需求增加Webpack的一些装备项(这儿是演示运用,在实践开发中,Webpack在production模式下会主动敞开这些字段,不需求运用者关心)。

// test/webpack.config.js
module.exports = {
    mode: 'none',
    entry: './index.js',
    output: {
        filename: 'index.js',
        path: __dirname + '/dist',
    },
    module: {
        rules: [{
            test: /.css$/,
            use: [
                'style-loader',
                'css-loader'
            ]
        },{
            test: /.js$/,
            use: 'babel-loader'
        },]
    },
    // webpack敞开tree-shaking需求的装备
    optimization: {
        usedExports: true,
        minimize: true,
    }
}

再次打包咱们的工程能够得到如下代码(因为minimize装备默许运用了Terser进行代码压缩,因而下面的文件难以读懂)。

(()=>{"use strict";var e,t=[,(e,t,o)=>{function n(e,t){void 0===t&&(t={});var o=t.insertAt;if(e&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],r=document.createElement("style");r.type="text/css","top"===o&&n.firstChild?n.insertBefore(r,n.firstChild):n.appendChild(r),r.styleSheet?r.styleSheet.cssText=e:r.appendChild(document.createTextNode(e))}}function r(){return"button"}o.d(t,{Button:()=>r}),n(".button{n    background-color: white;n}"),n(".alert{n    background-color: black;n}")}],o={};function n(e){var r=o[e];if(void 0!==r)return r.exports;var d=o[e]={exports:{}};return t[e](d,d.exports,n),d.exports}n.d=(e,t)=>{for(var o in t)n.o(t,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e=n(1),console.log(e.Button)})();

能够看到咱们的代码中关于Alert组件的js逻辑现已被Tree-Shaking删去了。不过仔细的同学应该发现了Alert的款式代码并没有被除去!这是因为Tree-Shaking只对JS代码起作用。

定论

Tree-Shaking能够做到组件库逻辑代码的按需引进,实践上闻名代码库AntD现在便是经过Tree-Shaking完成的。不过与咱们的测验成果相同,它对CSS并没有方法处理,这也是其文档中关于CSS按需引进丝毫不提的原因!

从组件按需引进深化前端打包构建

不过咱们其实是能够经过其它手法往来不断除非必要的CSS代码,例如经过装备purgecss-webpack-plugin抛弃一切未运用的CSS,不过这并不在本文的讨论范围内。

能够看到,经过运用Tree-Shaking咱们完成了组件库JS代码的按需引进,比较于Babel-plugin-import的方法,咱们更近一步的削减了用户的负担,运用者乃至底子就不需求更改一行代码!可是它的缺陷也是显着的,不支撑ESM以外的导入方法(不过现在看来这不是什么大问题,谁还在用其它方法呢?),不支撑CSS的按需引进。

六. 终极手法 —— Monorepo

终究,咱们进入到完成按需引进的终极方法——Monorepo!为什么说它是终极方法呢?因为经过Monorepo形式安排的组件库在发布时每个组件都是一个独自的NPM包,用户能够自己选择期望运用的组件并装置(你没听错,乃至都不必装置整个组件库),这样自然做到了完彻底全的按需引进(乃至能够说从底子上干掉了这个概念)!

不过,不要了解错了,Monorepo的呈现并不是为了处理组件按需引进的问题的。

Monorepo OR Multirepo?

尽管许多同学或许并不了解上面这两个词,可是咱们在实践的开发中肯定运用过这其间的概念!简略地说,Multirepo便是指不同项目的代码放在不同的git代码库房中(说用过没骗你吧?),而Monorepo则是与其相反,将不同项目的代码放在同一个库房之中。

在具体的开发实践中,这两种代码安排方法其实各有利弊。现在在大多企业中,咱们的项目代码都是遵循Multirepo的原则。你或许会想,咱们为什么需求Monorepo呢?不同项目的代码不就应该隔离开么?其实不然,咱们假定存在这样的一个场景,公司里有A、B两个项目一起在开发,可是在开发进程中项目人员发现这两个项目都会运用一个C组件。这时就有两种处理思路:

  1. 继续运用Multirepo:产品忽然提出了一个对C组件的新需求,这时咱们需求先拉取C的代码,更新C组件,在私有NPM源中发布(为了不走漏公司秘要。不过许多公司或许还没有建立自己的NPM源,这样的话就更费事了,需求频频拉取各文件并履行npm link),再拉取并打开A项目更新A项目的依靠,从头打包A项目并发布,之后拉取并打开B项目更新B项目的依靠,从头打包B项目并发布(光描绘这个进程就足够让人感到头疼了!)。
  2. 运用Monorepo:产品忽然提出了一个对C组件的新需求,这时咱们拉取整个项目的代码,更新C组件,然后运转奇特的lerna build指令。奇观发生了,咱们的A、B两个项目都主动更新了C包的依靠而且打包成功了!咱们只需求终究发布一下它们就能够了。

其实运用Monorepo的优势远不止上述的这一种场景,网上现已存在大量讲述其优点并介绍其运用方法的文章了。不过一起也要记住Monorepo并不是银弹,它同样是有缺陷的——如增加代码体积及初次装备的杂乱性、不同packages之间的访问权限难以操控等问题,因而并不适用于一切场景。这儿咱们不再深化对比起好坏,咱们仅仅期望运用它来协助咱们完成组件按需引进的需求。

改造组件库

为了更贴近于Monorepo的代码安排形式,咱们来改造一下第二节中虚拟组件库的代码。

首要要理解,关于Monorepo形式的组件库来说,咱们应该将每个组件都抽取出来作为一个独自的package存在,也便是说每个组件都应该有自己独立的源文件以及package.json。别的,作为一个组件库,咱们当然不能只支撑按需引进,也需求存在一个能够让用户一次性引进悉数组件的包。

组件库的代码安排形式如下:

┌ packages              子项目库
| | Main                悉数组件的集合
| | Alert               Alert组件
| ├ Button             	Button组件
| | | dist              打包后的组件
| | ├ lib               组件源文件
| | | ├ style           
| | | | └ index.css   
| | └ index.js
| └ package.json      
| lerna.json            Lerna装备
| package.json         
└ rollup.config.js      Rollup打包装备

关于Button和Alert来说,其内部代码与之前并无不同。有同学或许会猎奇Main代码是怎样编写的。其实非常简略,便是把组件库中的一切组件都作为dependency引进,然后再导出即可。

// mycompotest-mono/packages/Main/lin/index.js
import Button from '@my-compo-test/button'
import Alert from '@my-compo-test/alert'
export default {
    Button,
    Alert
};

Yarn + Lerna的魔法

搜索Monorepo实践,能够看到大部分处理计划都是基于Yarn+Lerna的方法(其实也存在PNPM等其它完成方法)。

深化研究能够看出为了完成一个Monorepo安排方法的组件库,咱们不只需求依靠比较NPM而言对Monorepo支撑更好的Yarn作为包办理东西(当然运用NPM也不是不行,不过稍费事一些),还需求依靠Lerna为咱们供给主动化的包版别晋级及发布的支撑。

首要咱们树立一个包含一切组件的目录packages并将其途径作为package.json文件中workspaces字段的值,这是为了告知Yarn咱们运用了Monorepo的代码安排方法。Yarn在拿到这个字段后,会将该目录中的每个子文件夹都作为一个独自的包处理。这样做的主要优点有以下两点:

  1. 协助你更好地办理多个子package的repo,这样你能够在每个子package里运用独立的package.json办理其依靠,又不必分别进到每一个子package里去yarn install/upgrade装置/晋级依靠,而是运用一条yarn指令在根目录下去处理一切依靠。简略说便是能够让你像办理一个package相同去办理多个packages。
  2. Yarn会根据依靠联系协助你剖析一切子packages的共用依靠,确保packages共用的依靠只会被下载和装置一次(具体做法是将其共同依靠提高至根目录下的node_modules),从而减缩依靠的装置时间,削减依靠的占用空间。
// mycompotest-mono/package.json
{
  "name": "mycompotest-mono",
  // 符号根目录为私有
  "private": true,
  // 留意: Monorepo重要字段
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
     // 假如运用了NPM作为包办理东西,需求以下指令
    "postinstall": "lerna bootstrap",
    // 能够看到子package的打包和发布由Lerna接手了
    "build": "lerna run --stream --sort build",
    "pub": "lerna publish ----include-merged-tags"
  },
  "devDependencies": {
    "postcss": "^8.3.9",
    "postcss-cli": "^9.0.1",
    "rollup": "^2.58.0",
    "rollup-plugin-postcss": "^4.0.1"
  },
  "dependencies": {
    "@rollup/plugin-commonjs": "^21.0.0",
    "@rollup/plugin-node-resolve": "^13.0.5"
  },
  "author": "",
  "license": "ISC"
}

接下来,咱们还需求对Lerna进行一些装备。与大多数的东西相同,Lerna的装备文件默许也在根目录下。

// mycompotest-mono/lerna.json
{
  	// 包办理模式
    "version": "independent",
    // 包办理东西
    "npmClient": "yarn",
    "useWorkspaces": true,
    // 子包目录
    "packages": [
      "packages/*"
    ]
}

之后咱们加入Rollup的装备文件。

// mycompotest-mono/rollup.config.js
import postcss from 'rollup-plugin-postcss';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
const fs = require('fs');
const pkJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
export default {
    input: 'lib/index.js',
    output: {
      file: 'dist/index.js',
      format: 'umd',
      name: pkJson._globalName
    },
    plugins: [
        nodeResolve(),
        postcss(),
        commonjs(),
    ]
}

运转yarn build能够看到如下提示,证明咱们将Alert/Button/Main悉数打包成功。

从组件按需引进深化前端打包构建

之后运转yarn pub,这个指令会协助咱们以一种问答的方法主动更新子packages的版别号及其依靠并发布到NPM源之中(为了NPM源的干净,这儿不再演示发布流程)。

Lerna是怎样做到的

你或许会猎奇一个Lerna究竟是怎样在咱们的项目中施放“咒语”的,这儿咱们简略的解说一下其大致原理。

  • Lerna怎样进行悉数打包

    你或许会想,这个很简略,不便是进入每个子package中依次履行它们的build指令么。没错,Lerna大体上便是这么做的。不过需求留意的是这个“依次”或许并不像你想象的那么简略。要理解,在咱们的项目中,Main包是依靠于Alert和Button两个组件包之上的,因而在打包Main包时需求留意,一定要先完成Alert/Button组件的打包才干够打包Main。这个次序是非常重要的,违反次序的打包会造成不行预料的错误。为了能够按序打包,Lerna内置了能够剖析包之间依靠联系的逻辑,这使得咱们只需求加上–sort参数便能够操控以拓扑排序规矩履行子package中的指令。

  • Lerna pub怎样主动更新包版别

    Lerna的这点使其显得愈加奥秘了,不过咱们只需求稍稍深化一点,就能够知道完成这个功用并没有那么杂乱。在Git办理的项目中,Lerna在每次发布包时会增加一条commit记载而且主动打上相关的tag信息。在下次再运转lerna pub指令时,Lerna会找到上次打标的tag信息,拿出其间的文件与当时文件做对比。假如前后文件一致,则Lerna默许其没有更改,不需求从头发布。假如前后文件不同,则Lerna就会判别这个文件所在的包需求更新版别号并从头发布。

  • Lerna怎样主动更新依靠

    其实有了对以上“魔法”的研究,这个“魔法”只不过是瓜熟蒂落的才能算了。Lerna先找出需求更新包版别的packages,然后找出依靠这些packages的其它packages并修改它们所依靠的版别,不断递归这一进程,终究Lerna就能够主动更新相关依靠了。

引进组件库

言归正传,至此咱们作为组件库开发者的作业现已悉数完成。换个视点,作为组件库运用者,咱们应该怎样运用这个现已发布的组件库呢?(留意:上述组件库并未发布,运用npm link模仿引进)

来看咱们的首要方针,Button组件的按需引进。

// test/index.js
import Button from '@my-compo-test/button'
console.log(Button)

能够看到,项目只关注于引进组件库中的Button组件。因为运用了Monorepo的形式安排了组件库,运用者能够单纯的不止能够单纯的只引进Button这个组件的逻辑和款式,而且比较于Babel-plugin-import而言,乃至都不必下载Alert组件的任何代码!(这儿咱们不再打包调查dist中的文件,任何一个有实践开发经历的前端程序员都应该都理解为什么没有这个必要!)

当然,假如运用者固执的以为你会用到组件库中的悉数组件,那么咱们也为他供给了Main包。

// test/index.js
import {Button} from '@my-compo-test/main'
console.log(Button)

定论

经过Monorepo的代码安排形式,咱们同样完成了组件按需引进的需求。而且比较于以上几种方法,由Monorepo完成的组件按需引进愈加朴实,运用者不只不会引进其它组件的JS以及CSS代码,乃至连下载这一步都省掉了!别的,这种方法也防止了关于运用者代码的侵入,无需为按需引进功用增加其它杂乱的装备。

七、总结

祝贺坚持读到这儿的你,这篇冗长的文章终于要结束了!在终究,咱们把之前说到的几种方法做一个对比,信任我,下面这个表格和上面几节的内容相同重要!

手动引进 Babel-plugin-import Tree-Shaking Monorepo
能否只引进需求的JS
能否只引进需求的CSS 不能
用户是否需求额定的装备或了解 需求了解组件库目录结构 需求装备Babel 不需求(webpack生产模式默许装备) 不需求
有背书的闻名组件库么 Vant Element-UI/iView/Vant AntD 暂无

终究的终究,你能够从头回到榜首节,再次答复一下文章开头面试官所说到的问题!

学习前端工程化的道路是漫长且枯燥的,期望这篇文章能帮到正在前进的你。