umi-next 浅析(未完)

577 阅读3分钟

babel-preset-umi

提供 umi 的 babel preset,其中大部分presets 跟 plugins 都是依赖 bundler utils 内编译好的,同时也有自己编写的三个babel 插件

  1. autoCSSModules 解决 css 自动引入 module 问题, 通过 babel 插件编译,在路径上添加?modules 告诉打包工具需要使用module loader
const CSS_EXT_NAMES = ['.css', '.less', '.sass', '.scss', '.stylus', '.styl'];

export default function () {
  return {
    visitor: {
      ImportDeclaration(path: Babel.NodePath<t.ImportDeclaration>) {
        const {
          specifiers,
          source,
          source: { value },
        } = path.node;
        if (specifiers.length && CSS_EXT_NAMES.includes(extname(value))) {
          source.value = `${value}?modules`;
        }
      },

      // e.g.
      // const styles = await import('./index.less');
      VariableDeclarator(path: Babel.NodePath<t.VariableDeclarator>) {
        const { node } = path;
        if (
          t.isAwaitExpression(node.init) &&
          t.isCallExpression(node.init.argument) &&
          t.isImport(node.init.argument.callee) &&
          node.init.argument.arguments.length === 1 &&
          t.isStringLiteral(node.init.argument.arguments[0]) &&
          CSS_EXT_NAMES.includes(extname(node.init.argument.arguments[0].value))
        ) {
          node.init.argument.arguments[0].value = `${node.init.argument.arguments[0].value}?modules`;
        }
      },
    },
  };
}

2.dynamicImportNode 修改import 语句,改成动态import ,同时会移除import path 前面的注释,import('a');改成 Promise.resolve().then(() => _interopRequireWildcard(require('a')));

const builders = {
  static: template('Promise.resolve().then(() => INTEROP(require(SOURCE)))'),
  dynamic: template('Promise.resolve(SOURCE).then(s => INTEROP(require(s)))'),
};

function isString(node: t.Node) {
  return (
    t.isStringLiteral(node) ||
    (t.isTemplateLiteral(node) && node.expressions.length === 0)
  );
}

export default (): Babel.PluginObj => {
  const visited = new WeakSet();
  return {
    visitor: {
      Import(path: Babel.NodePath<t.Import>) {
        if (visited) {
          if (visited.has(path)) {
            return;
          }
          visited.add(path);
        }

        const SOURCE = getImportSource(path.parent);
        const builder = isString(SOURCE) ? builders.static : builders.dynamic;
        const newImport = builder({
          SOURCE,
          INTEROP: path.hub.addHelper('interopRequireWildcard'),
        });
        // @ts-ignore
        path.parentPath.replaceWith(newImport);
      },
    },
  };
};

3.lockCorejs 改写 lockCorejs 路径

bundler-esbuild

  1. 使用 esbuild 编译代码,只支持 build,不支持 dev,
if (command === 'build') {
  (async () => {
    process.env.NODE_ENV = 'production';
    assert(entry, `Build failed: entry not found.`);
    try {
      await build({
        config,
        cwd,
        entry: {
          [getEntryKey(entry)]: entry,
        },
      });
    } catch (e) {
      console.error(e);
    }
  })();
} else {
  error(`Unsupported command ${command}.`);
}
  1. 从四个默认的地址寻找entry
const entry = tryPaths([
  join(cwd, 'src/index.tsx'),
  join(cwd, 'src/index.ts'),
  join(cwd, 'index.tsx'),
  join(cwd, 'index.ts'),
]);

3.利用 pirates 去 hack require hook,让 esbuild 去解析配置文件

esbuild 插件部分

  1. external, 把options 中 key 的值暴露给 module.exports
export default (options?: Record<string, string>): Plugin => {
  return {
    name: 'externals',
    setup({ onLoad, onResolve }) {
      if (!options || Object.keys(options).length === 0) {
        return;
      }
      Object.keys(options).forEach((key) => {
        onResolve({ filter: new RegExp(`^${key}$`) }, (args) => ({
          path: args.path,
          namespace: key,
        }));
        onLoad({ filter: /.*/, namespace: key }, () => ({
          contents: `module.export=${options[key]}`,
          loader: 'js',
        }));
      });
    },
  };
};
  1. style, 主要是寻找、解析 css ,如果是inline,还添加辅助方法,创建style标签插入 body,而且loader 为text,否则不是inline,loader 为css, injectStyle 这个方法很通用, less inline 编译后插入也是复用这个方法
if (inlineStyle) {
  onResolve({ filter: /.css$/, namespace: 'style-stub' }, (args) => {
    return { path: args.path, namespace: 'style-content' };
  });

  onResolve(
    { filter: /^__style_helper__$/, namespace: 'style-stub' },
    (args) => ({
      path: args.path,
      namespace: 'style-helper',
      sideEffects: false,
    }),
  );

  onLoad({ filter: /.*/, namespace: 'style-helper' }, async () => ({
    contents: `
      export function injectStyle(text) {
        if (typeof document !== 'undefined') {
          var style = document.createElement('style')
          var node = document.createTextNode(text)
          style.appendChild(node)
          document.head.appendChild(style)
        }
      }
    `,
  }));

  onLoad({ filter: /.*/, namespace: 'style-stub' }, async (args) => ({
    contents: `
      import { injectStyle } from "__style_helper__"
      import css from ${JSON.stringify(args.path)}
      injectStyle(css)
    `,
  }));
}
  1. alias 在 resolve 的时候寻找alias对应的路径解析回去,同时还支持文件夹
export default (options: Record<string, string> = {}): Plugin => {
  return {
    name: 'alias',
    setup({ onResolve }) {
      const keys = sortByAffix({ arr: Object.keys(options), affix: '$' });
      keys.forEach((key) => {
        let value = options[key];
        let filter: RegExp;
        if (key.endsWith('$')) {
          filter = new RegExp(`^${key}`);
        } else {
          filter = new RegExp(`^${key}$`);
        }
        onResolve({ filter: filter }, async (args) => {
          const path = await resolve(
            args.importer,
            args.path.replace(filter, value),
          );
          return {
            path,
          };
        });

        if (
          !key.endsWith('/') &&
          existsSync(value) &&
          statSync(value).isDirectory()
        ) {
          const filter = new RegExp(`^${addSlashAffix(key)}`);
          onResolve({ filter }, async (args) => {
            const path = await resolve(
              args.importer,
              args.path.replace(filter, addSlashAffix(value)),
            );
            return {
              path,
            };
          });
        }
      });
    },
  };
};

function addSlashAffix(key: string) {
  if (key.endsWith('/')) {
    return key;
  }
  return `${key}/`;
}

bundler-vite

提供vite 的dev 和 build 配置,

bundler-utils

  1. https 主要用于读取 https 证书创建 https 服务器,没有证书就自己创建
export async function resolveHttpsConfig(httpsConfig: HttpsServerOptions) {
  // Check if mkcert is installed
  try {
    await execa.execa('mkcert', ['--version']);
  } catch (e) {
    logger.error('[HTTPS] The mkcert has not been installed.');
    logger.info('[HTTPS] Please follow the guide to install manually.');
    switch (process.platform) {
      case 'darwin':
        console.log(chalk.green('$ brew install mkcert'));
        console.log(chalk.gray('# If you use firefox, please install nss.'));
        console.log(chalk.green('$ brew install nss'));
        console.log(chalk.green('$ mkcert -install'));
        break;
      case 'win32':
        console.log(
          chalk.green('Checkout https://github.com/FiloSottile/mkcert#windows'),
        );
        break;
      case 'linux':
        console.log(
          chalk.green('Checkout https://github.com/FiloSottile/mkcert#linux'),
        );
        break;
      default:
        break;
    }
    throw new Error(`[HTTPS] mkcert not found.`);
  }

  let { key, cert, hosts } = httpsConfig;
  hosts = hosts || defaultHttpsHosts;
  if (!key || !cert) {
    key = join(__dirname, 'umi.key.pem');
    cert = join(__dirname, 'umi.pem');
  }

  // Generate cert and key files if they are not exist.
  if (!existsSync(key) || !existsSync(cert)) {
    logger.wait('[HTTPS] Generating cert and key files...');
    await execa.execa('mkcert', [
      '-cert-file',
      cert,
      '-key-file',
      key,
      ...hosts!,
    ]);
  }

  return {
    key,
    cert,
  };
}
  1. 提供 利用esbuild 跟 es-module-lexer 提供代码解析功能,可以看到利用提前编译好的库,防止 semver版本问题以及break changes
import { init, parse } from '@umijs/bundler-utils/compiled/es-module-lexer';
import { Loader, transformSync } from '@umijs/bundler-utils/compiled/esbuild';
  1. 对内提供monorepo需要的编译好的依赖库

msfu

主要利用 webpack 5 module fedoration 的remote 以及 webpack virtrual module ,分析依赖,让依赖指向编译好的包,对于 webpack 一些内置依赖,还利用 require hook 进行 hack,(待添加详细笔记)

server

暂时只看到生成 spa 的 markup html 以及生成路由配置, 1.下面是生成 markup html

export async function getMarkup(
  opts: Omit<IOpts, 'routes'> & {
    path?: string;
  },
) {
  // TODO: use real component
  let markup = ReactDOMServer.renderToString(
    React.createElement('div', { id: opts.mountElementId || 'root' }),
  );

  function propsToString(opts: {
    props: Record<string, any>;
    filters?: string[];
  }) {
    return Object.keys(opts.props)
      .filter((key) => !(opts.filters || []).includes(key))
      .map((key) => `${key}=${JSON.stringify(opts.props[key])}`)
      .join(' ');
  }

  function getScriptContent(script: { src?: string; content?: string }) {
    const attrs = propsToString({
      props: script,
      filters: ['src', 'content'],
    });
    return script.src
      ? `<script${opts.esmScript ? ' type="module"' : ''} ${attrs} src="${
          script.src
        }"></script>`
      : `<script${opts.esmScript ? ' type="module"' : ''} ${attrs}>${
          script.content
        }</script>`;
  }

  function getStyleContent(style: { src?: string; content?: string }) {
    const attrs = propsToString({
      props: style,
      filters: ['src', 'content'],
    });
    return style.src
      ? `<link rel="stylesheet" ${attrs} href="${style.src}" />`
      : `<style ${attrs}>${style.content}</style>`;
  }

  function getTagContent(opts: {
    attrs: Record<string, string>;
    tagName: string;
  }) {
    const attrs = propsToString({
      props: opts.attrs,
    });
    return `<${opts.tagName} ${attrs} />`;
  }

  const favicons: string[] = [];
  if (Array.isArray(opts.favicons)) {
    opts.favicons.forEach((e) => {
      favicons.push(`<link rel="shortcut icon" href="${e}">`);
    });
  }
  const title = opts.title ? `<title>${opts.title}</title>` : '';
  const metas = (opts.metas || []).map((meta) =>
    getTagContent({ attrs: meta, tagName: 'meta' }),
  );
  const links = (opts.links || []).map((link) =>
    getTagContent({ attrs: link, tagName: 'link' }),
  );
  const styles = normalizeStyles(opts.styles || []).map(getStyleContent);
  const headScripts = normalizeScripts(opts.headScripts || []).map(
    getScriptContent,
  );
  const scripts = normalizeScripts(opts.scripts || []).map(getScriptContent);
  markup = [
    `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
  name="viewport"
  content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />`,
    metas.join('\n'),
    favicons.join('\n'),
    title,
    links.join('\n'),
    styles.join('\n'),
    headScripts.join('\n'),
    `</head>
<body>`,
    markup,
    scripts.join('\n'),
    `</body>
</html>`,
  ]
    .filter(Boolean)
    .join('\n');
  if (opts.modifyHTML) {
    markup = await opts.modifyHTML(markup, { path: opts.path });
  }
  return markup;
}
  1. dfs 优先,生成路由配置,把数组转换成树形结构
export function createServerRoutes(opts: {
  routesById: IRoutesById;
  parentId?: string;
}) {
  const { routesById, parentId } = opts;
  return Object.keys(routesById)
    .filter((id) => routesById[id].parentId === parentId)
    .map((id) => {
      const route = createServerRoute({
        route: routesById[id],
      });
      const children = createServerRoutes({
        routesById,
        parentId: route.id,
      });
      if (children.length > 0) {
        // @ts-ignore
        route.children = children;
      }
      return route;
    });
}

export function createServerRoute(opts: { route: IRoute }) {
  const { route } = opts;
  return {
    id: route.id,
    path: route.path,
    index: route.index,
  };
}

umi

  1. 提供cli,走 core 包的 插件机制,
  • cli 解析命令,同时会进行一系列检测,如node版本,设置 proces 的标题等
  • 经典 node 中断监听
// kill(2) Ctrl-C
process.once('SIGINT', () => onSignal('SIGINT'));
// kill(3) Ctrl-\
process.once('SIGQUIT', () => onSignal('SIGQUIT'));
// kill(15) default
process.once('SIGTERM', () => onSignal('SIGTERM'));
  • dev 的命令加一层监听之后,底层还是调用 core 的service command,其他命令都是直接调用core 的service command
  • dev 调用 fork, fork 调用 fork 出 forkdev,fork 提供了重启功能,说白了,就是监听到重启命令,杀掉child,重新 fork出新的forkdev,fork还提供了断点功能,就是inspect-brk
  1. 提供 client 层的插件机制
  • 支持三种类型的机制,说白了就是三种机制对存储的同个key的hook怎么去调用,或者compose,或者reduce,或者干脆顺序执行,即event。同时如果hook是async方法,但是调用的时候不指明async 的options,将不await 方法执行完毕。
  1. 提供 core 的service 层
  2. 定义配置文件
  3. 对外暴露内置各种功能,如 pluginUtils,eslint配置,stylelint噢诶之等