龙空技术网

father-build 使用总结

游戏电影常伴吾身 347

前言:

现在小伙伴们对“jquerytree异步加载”大致比较注重,小伙伴们都想要了解一些“jquerytree异步加载”的相关资讯。那么小编同时在网上汇集了一些关于“jquerytree异步加载””的相关资讯,希望我们能喜欢,你们一起来了解一下吧!

一、介绍

umijs/father 是个由 lerna 管理的,基于 rollup 和 babel 的工具库。组件打包功能主要是 packages 下的 father-build 实现的。如果只做组件打包,不需要文档功能,可直接安装 father-build,使用和配置同 father。

1.1 father 特性

✔︎ 基于 docz 的文档功能(不再维护,建议 迁移到 dumi 或 单独安装 docz 使用)

✔︎ 基于 rollup 和 babel 的组件打包功能

✔︎ 支持 TypeScript

✔︎ 支持 cjs、esm 和 umd 三种格式的打包

✔︎ esm 支持生成 mjs,直接为浏览器使用

✔︎ 支持用 babel 或 rollup 打包 cjs 和 esm

✔︎ 支持多 entry

✔︎ 支持 lerna

✔︎ 支持 css 和 less,支持开启 css modules

✔︎ 支持 test

✔︎ 支持用 prettier 和 eslint 做 pre-commit 检查

1.2 CJS、ESM 和 UMD它们是什么?

它们是在 JS 里用来实现“模块”的不同规则。

CJS

CJS 就是 CommonJS 规范的缩写。

语法:

// doSomething.js// 导出module.exports = function doSomething(n) {  // 做点啥}// 引入 const doSomething = require('./doSomething.js'); 

特点:

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即module.exports)是对外的接口。 加载某个模块,其实是加载该模块的module.exports属性。模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。模块加载的顺序,按照其在代码中出现的顺序。值得一提的是,CommonJS 规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。UMD

UMD 是 Universal Module Definition(通用模块定义) 的缩写。

语法:

(function (root, factory) {    if (typeof define === "function" && define.amd) {        // AMD        define(["jquery", "underscore"], factory);    } else if (typeof exports === "object") {        // CommonJS 等        module.exports = factory(require("jquery"), require("underscore"));    } else {        // 浏览器全局变量(root 即 window)        root.Requester = factory(root.$, root._);    }}(this, function ($, _) {    // 方法    function a() {}; // 私有方法,因为它没有被返回(见下面)    function b() {}; // 公共方法,因为被返回了    function c() {}; // 公共方法,因为被反会了    // 暴露公共方法    return {        b: b,        c: c    }}));

特点:

UMD 是为了让模块同时兼容 AMD 和 CommonJS 规范而出现的,多被一些需要同时支持浏览器端和服务端引用的第三方库所使用,UMD是一个时代的产物。可以使用<script>标签直接引用。通常在 ESM 不起作用的情况下用作备用 。ESM

ESM 就是 ECMAScript Module 的缩写。

语法:

// 导出export default function() {  // 做点啥};export const foo() {...};export const bar() {...};// 引入import {foo, bar} from './myLib';

特点:

ESM规范是ES标准的模块化规范它兼具两方面的优点:具有 CJS 的简单语法和 AMD 的异步CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import 时采用静态命令的形式。即在 import 时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

模块规范

CJS

(CommonJS )

UMD

(Universal Module Definition)

ESM

(ECMAScript Module)

特点

同步加载不支持 tree-shaking可以使用<script>标签引用异步加载支持 tree-shaking

运行环境

仅 NodeJS

NodeJS、浏览器

NodeJS、浏览器

father-build 打包目录

lib 目录

dist 目录

es 目录

相关配置package.json 发行打包相关参数main:定义一个入口文件module:定义一个针对 es6 模块及语法的入口文件unpkg:unpkg 是一个前端常用的公共CDN 服务。配置了这个参数,可以让上传到 npm的所有文件都开启 unpkg 的 cdn 服务。webpack 的 target 属性

含义:由于 JavaScript 既可以编写服务端代码也可以编写浏览器代码,所以 webpack 提供了 target 属性,用来制定构建目标。

默认值:当配置了 browserslist 的时候,默认值是 "browserslist" ;否则就是 "web" 。

webpack.config.js

module.exports = {  target: 'node',};

webpack 的 resolve.mainFields 属性

含义:当从 npm 包中导入模块时(例如,import * as D3 from 'd3'),此选项将决定在 npm 包的 package.json 中使用哪个字段导入模块。

默认值:根据 webpack 配置中指定的 target 不同,默认值也会有所不同。

当 target 属性设置为 webworker, web 或者没有指定的话:

webpack.config.js

module.exports = {  //...  resolve: {    mainFields: ['browser', 'module', 'main'],  },};

对于其他任意的 target(包括 node),默认值为:

webpack.config.js

module.exports = {  //...  resolve: {    mainFields: ['module', 'main'],  },};

项目在解析依赖包的文件时,是按照 mainFields 中属性的顺序决定优先级。

比如对于以上 antd 的 package.json 中的配置,如果我们项目中 target 没有指定,那默认会先找 browser 配置的入口文件,没有的话,再找 module 属性配置的文件。

1.3 常用配置

以下是 公司项目的 father 配置:

import {readdirSync} from 'fs';import {join} from 'path';const headPkgs: string[] = [    'emotion',     ...];const tailPkgs = [];const type = process.env.BUILD_TYPE;let config = {};if (type === 'es') {  config = {    // 是否输出 cjs 格式,以及指定 cjs 格式的打包方式等。    cjs: false,    // 是否输出 esm 格式,以及指定 esm 格式的打包方式等。    esm: {      // 指定 esm 的打包类型,可选 rollup 或 babel。      type: 'rollup',      // 是否在 esm 模式下把 import 项里的 /lib/ 转换为 /es/。      // 比如 import 'foo/lib/button';,在 cjs 模式下会保持原样,在 esm 模式下会编译成 import 'foo/es/button';。      importLibToEs: true,    },    // 是否把 helper 方法提取到 @babel/runtime 里。    // 推荐开启,能节约不少尺寸    // runtimeHelpers 只对 esm 有效,cjs 下无效,因为 cjs 已经不给浏览器用了,只在 ssr 时会用到,无需关心小的尺寸差异    runtimeHelpers: true,    // 自定义 packages 目录下的构建顺序    pkgs: [...headPkgs, ...tailPkgs],    // 配置是否开启 css modules。    // 如果组件中用了 css modules,但不开启这个配置,会导致样式引入失败    // 虽然 father 提供这个能力,但不建议为组件库启用 CSS Modules,    // 这将使得组件库用户很难覆写样式,下一版的 father 也将移除该特性。    cssModules: true,    // 在 rollup 模式下做 less 编译,支持配置 less 在编译过程中的 Options    lessInRollupMode: {      javascriptEnabled: true,    },    // 配置额外的 babel plugin    extraBabelPlugins: [      ['babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'],    ],    // 是否禁用类型检测。    disableTypeCheck: true  };}export default config;
二、调试

那 father-build 是如何工作的呢?

先来建一个项目(umi 搭建一个) 。使用 vscode 来调试,配置 launch.json

{    "version": "0.2.0",    "configurations": [           {            "type": "node",            "request": "launch",            "name": "Debug father",            "skipFiles": [                "<node_internals>/**"            ],            "runtimeExecutable": "npm",            "runtimeArgs": [                "run-script",                "build"            ]        }    ]}
点击 debug,开始调试三、源码分析

在前面的调试中,我们可以看到,入口文件是 bin/father-build.js,在 father-build 中,支持通过命令行传递参数。相应的源码如下:

 const args = yParser(process.argv.slice(2)); const buildArgs = stripEmptyKeys({    esm: args.esm && { type: args.esm === true ? 'rollup' : args.esm },    cjs: args.cjs && { type: args.cjs === true ? 'rollup' : args.cjs },    umd: args.umd && { name: args.umd === true ? undefined : args.umd },    file: args.file,    target: args.target,    entry: args._,    config: args.config,  });

比如 node ./bin/father-build.js --esm --cjs --umd --file bar ./src/index.js ,然后通过 yargs-parser 进行解析,得到结果为

然后进入到 build.ts文件,判断是否使用了 lerna,然后根据是否是 lerna来调整打包的逻辑。代码如下:

 const useLerna = existsSync(join(opts.cwd, 'lerna.json')); const isLerna = useLerna && process.env.LERNA !== 'none'; const dispose = isLerna ? await buildForLerna(opts) : await build(opts); 
先来看一下非 lerna 模式的打包逻辑:首先使用了 babel-register。使用 babel-register 之后,后续被 node 使用 require 语法引用的文件,都会被 babel 进行代码转换。获取打包配置 getBundleOpts(opts) 。根据配置的 cjs、umd、esm 选项,来开始使用 babel 或者 rollup 进行打包。

代码逻辑如下:

export async function build(opts: IOpts, extraOpts: IExtraBuildOpts = {}) {  ...  // register babel for config files  registerBabel({    cwd,    only: customConfigPath ? CONFIG_FILES.concat(customConfigPath) : CONFIG_FILES,  });  // Get user config  const bundleOptsArray = getBundleOpts(opts);  for (const bundleOpts of bundleOptsArray) {    ...    // Build umd    if (bundleOpts.umd) {      log(`Build umd`);      await rollup({        cwd,        rootPath,        log,        type: 'umd',        entry: bundleOpts.entry,        watch,        dispose,        bundleOpts,      });    }    // Build cjs    if (bundleOpts.cjs) {      const cjs = bundleOpts.cjs as IBundleTypeOutput;      log(`Build cjs with ${cjs.type}`);      if (cjs.type === 'babel') {        await babel({ cwd, rootPath, watch, dispose, type: 'cjs', log, bundleOpts });      } else {        await rollup({          cwd,          rootPath,          log,          type: 'cjs',          entry: bundleOpts.entry,          watch,          dispose,          bundleOpts,        });      }    }    // Build esm    if (bundleOpts.esm) {      const esm = bundleOpts.esm as IEsm;      log(`Build esm with ${esm.type}`);      const importLibToEs = esm && esm.importLibToEs;      if (esm && esm.type === 'babel') {        await babel({ cwd, rootPath, watch, dispose, type: 'esm', importLibToEs, log, bundleOpts });      } else {        await rollup({          cwd,          rootPath,          log,          type: 'esm',          entry: bundleOpts.entry,          importLibToEs,          watch,          dispose,          bundleOpts,        });      }    }  }  return dispose;}
接下来看一下 father-build 内部是怎么获取用户配置的首先通过getExistFile() 来获取入口文件,内部是 fs 模块的 existsSync 方法判断入口文件是否存在,通常就是我们项目下 src/index.js。接着通过 getUserConfig() 来获取用户的配置信息,内部也是通过 fs 模块判断 .fatherrc.js, .fatherrc.jsx, fatherrc.ts', .fatherrc.tsx, .umirc.library.js, .umirc.library.jsx, umirc.library.ts, umirc.library.tsx 是否存在,存在的话,就读取里面的配置信息 ,通常就是我们项目下的配置的.fatherrc.js文件配置的打包参数。同时通过 ajv这个包,来对 schema.ts 中定义配置文件应该遵循的格式进行校验。根据获取的 userConfig 开始启用 babel 模式或者 rollup 模式打包。

接下来,看看获取到配置信息之后,father-build 是如何使用 babel 或者 rollup 打包的。

先来看一下 babel 的实现。

代码中硬编码了读取 src 目录,因此此时的 entry 配置是无效的。然后通过 pattern 找出需要编译的文件,进入到 createStream 方法

核心代码如下:

  function createStream(src) {    const tsConfig = getTSConfig();    const babelTransformRegexp = disableTypeCheck ? /\.(t|j)sx?$/ : /\.jsx?$/;    function isTsFile(path) {      return /\.tsx?$/.test(path) && !path.endsWith(".d.ts");    }    function isTransform(path) {      return babelTransformRegexp.test(path) && !path.endsWith(".d.ts");    }    return vfs      // 读取源文件      .src(src, {        allowEmpty: true,        base: srcPath,      })      // gulp-plumber这是一款防止因 gulp 插件的错误而导致管道中断,plumber 可以阻止 gulp 插件发生错误导致进程退出并输出错误日志。      .pipe(watch ? gulpPlumber() : through.obj())      .pipe(      // 先处理 ts        gulpIf((f) => !disableTypeCheck && isTsFile(f.path), gulpTs(tsConfig))      )      .pipe(        gulpIf(          // 处理 less 文件          (f) => lessInBabelMode && /\.less$/.test(f.path),          gulpLess(lessInBabelMode || {})        )      )      .pipe(        gulpIf(          (f) => isTransform(f.path),          through.obj((file, env, cb) => {            try {              file.contents = Buffer.from(               // 遇到 tsx, jsx 就用 babel 去处理               // transform 方法也就是根据 babel 配置来编译文件                transform({                  file,                  type,                })              );              // .jsx -> .js              file.path = file.path.replace(extname(file.path), ".js");              cb(null, file);            } catch (e) {              signale.error(`Compiled faild: ${file.path}`);              console.log(e);              cb(null);            }          })        )      )      // const srcPath = join(cwd, "src");      // const targetDir = type === "esm" ? "es" : "lib";      // const targetPath = join(cwd, targetDir);      .pipe(vfs.dest(targetPath));  }
再来看一下 rollup的实现。

如果选择使用 rollup 进行打包,那么代码就会先经过 rollup.ts 进入到 getRollupConfig.ts 中来,且在进入到 getRollupConfig 之前,会经过 normalizeBundleOpts 处理一些入参,比如处理 overridesByEntry 参数。到了 getRollupConfig.ts 中,就根据 type 来拼装 rollup 的参数, 包括组合 plugins,externals 来进行编译。

核心代码如下:

  switch (type) {    case 'esm':      const output: Record<string, any> = {        dir: join(cwd, `${esm && (esm as any).dir || 'dist'}`),        entryFileNames: `${(esm && (esm as any).file) || `${name}.esm`}.js`,      }          return [        {          input,          output: {            format,            ...output,          },          plugins: [...getPlugins(), ...(esm && (esm as any).minify ? [terser(terserOpts)] : [])],          external: testExternal.bind(null, external, externalsExclude),        },        ...(esm && (esm as any).mjs          ? [              {                input,                output: {                  format,                  file: join(cwd, `dist/${(esm && (esm as any).file) || `${name}`}.mjs`),                },                plugins: [                  ...getPlugins(),                  replace({                    'process.env.NODE_ENV': JSON.stringify('production'),                  }),                  terser(terserOpts),                ],                external: testExternal.bind(null, externalPeerDeps, externalsExclude),              },            ]          : []),      ];    case 'cjs':      return [        {          input,          output: {            format,            file: join(cwd, `dist/${(cjs && (cjs as any).file) || name}.js`),          },          plugins: [...getPlugins(), ...(cjs && (cjs as any).minify ? [terser(terserOpts)] : [])],          external: testExternal.bind(null, external, externalsExclude),        },      ];    case 'umd':      // Add umd related plugins      const extraUmdPlugins = [        commonjs({          include,          // namedExports options has been remove from         }),      ];      return [        {          input,          output: {            format,            sourcemap: umd && umd.sourcemap,            file: join(cwd, `dist/${(umd && umd.file) || `${name}.umd`}.js`),            globals: umd && umd.globals,            name: (umd && umd.name) || (pkg.name && camelCase(basename(pkg.name))),          },          plugins: [            ...extraUmdPlugins,            ...getPlugins(),            replace({              'process.env.NODE_ENV': JSON.stringify('development'),            }),          ],          external: testExternal.bind(null, externalPeerDeps, externalsExclude),        },        ...(umd && umd.minFile === false          ? []          : [              {                input,                output: {                  format,                  sourcemap: umd && umd.sourcemap,                  file: join(cwd, `dist/${(umd && umd.file) || `${name}.umd`}.min.js`),                  globals: umd && umd.globals,                  name: (umd && umd.name) || (pkg.name && camelCase(basename(pkg.name))),                },                plugins: [                  ...extraUmdPlugins,                  ...getPlugins({ minCSS: true }),                  replace({                    'process.env.NODE_ENV': JSON.stringify('production'),                  }),                  terser(terserOpts),                ],                external: testExternal.bind(null, externalPeerDeps, externalsExclude),              },            ]),      ];    default:      throw new Error(`Unsupported type ${type}`);  }
项目踩坑babel 模式默认不支持 cssModules。babel 模式打包资源放到 dist、es、lib 文件夹下,rollup 模式全部放到 dist 文件夹下(新的 father-build 的 esm 已经支持定义 dir,将打包资源放到自定义文件夹下,但是esm.mjs格式的资源还不能自定义,还是打包到 dist 文件夹下面 )。boss 中 webpack 配置添加 module,因为 father-build 不同模式、打包的资源放到的文件夹不一样,要注意发包的时候 package.json 的资源引用配置,可能因为 webpack 配置resolve 添加的 module 导致资源找不到。五、参考链接

GitHub - umijs/father: Library toolkit based on rollup and babel.

标签: #jquerytree异步加载