近期,团(ling)队(dao)准(yao)备(qiu)从Vue技术栈转向React技术栈,并且特别指定了使用Ant Design的设计组件库,出于相关生态考虑,咱们决定采用蚂蚁金服团队相关react方案。选择理由以下:一来是React原装全家桶比较散,引包组装比较麻烦;二来是国内React生态相关方面阿里开源及社区作的比较突出,于是咱们决定使用阿里React相关技术栈。基于组件库及相关可视化展现等因素,咱们选择了蚂蚁金服团队的开源生态圈:umi + dva + antd + antv ( ps:最佳实践案例是Ant Desgin Pro),固然淘系的飞冰相关React技术栈作的也很突出,但权衡以后,咱们最终仍是选择了蚂蚁金服团队的React技术栈。做为整个生态圈最为核心的部分,umi可谓是王冠上的红宝石,于是我的认为对于整个umi架构内核的学习及设计哲学的理解,可能好比何使用要来的更为重要;做为一个使用者,但愿能从各位大佬的源码中汲取一些营养以及得到一些灵感,私觉得:思想的拓展要远比不断地重复劳做要来的重要!javascript
umi基于的是一种微内核架构方式,其核心是只保留架构的核心功能,将其余需求服务以插件的形式载入进来,“即插即用,不用即走”,于是又称为“插件化架构”,对于想具体了解微内核的童鞋,能够看这篇文章微内核架构,总结来说就是“如无必要,勿增实体”,只保留最精简、最核心的部分。css
对于umi来讲,其具体是经过“约定大于配置”的核心理念,将技术收敛,让开发者更集中精力于业务的开发,对于更具体的能够看看云谦(陈成)的大佬2019 SEE Conf的分享 云谦 - 蚂蚁金服前端框架探索之路前端
本质上umi最后会导出一个基于EventEmitter的Service的类,用户经过Config中的配置将插件与核心Service关联,umi将react-router和react-router-dom内嵌进了框架中,从而能够“约定式定义路由”,此处能够对比Next.js的方案java
packagesnode
corereact
srcwebpack
核心源码在于core目录下的Config、Route及Service,微内核中最最最核心的就是这个Service类,其余都是基于其进行的相关的扩展与融合,重点分析Service、Route及Config这三个目录中的源码git
文件名 | 做用 | 备注 |
---|---|---|
Service.ts | 提供整个核心服务类,用于导出服务 | 核心配置 |
getPaths.ts | 获取文件绝对路径的核心方法 | 文件路径 |
PluginAPI.ts | 插件的注册及接入核心类 | 插件注册 |
types.ts | 固定值 | 接口及类型 |
enums.ts | 固定值 | 枚举 |
export default class Service extends EventEmitter { // 项目根路径 cwd: string; // package.json的绝对路径 pkg: IPackage; // 跳过的插件 skipPluginIds: Set<string> = new Set<string>(); // 生命周期执行阶段 stage: ServiceStage = ServiceStage.uninitialized; // 注册命令 commands: { [name: string]: ICommand | string; } = {}; // 解析完的插件 plugins: { [id: string]: IPlugin; } = {}; // 插件方法 pluginMethods: { [name: string]: Function; } = {}; // 初始化插件预设 initialPresets: IPreset[]; initialPlugins: IPlugin[]; // 额外的插件预设 _extraPresets: IPreset[] = []; _extraPlugins: IPlugin[] = []; // 用户配置 userConfig: IConfig; configInstance: Config; config: IConfig | null = null; // babel处理 babelRegister: BabelRegister; // 钩子函数处理 hooksByPluginId: { [id: string]: IHook[]; } = {}; hooks: { [key: string]: IHook[]; } = {}; // 用户配置生成的路径信息 paths: { // 项目根目录 cwd?: string; // node_modules文件目录 absNodeModulesPath?: string; // src目录 absSrcPath?: string; // pages目录 absPagesPath?: string; // dist导出目录 absOutputPath?: string; // 生成的.umi目录 absTmpPath?: string; } = {}; env: string | undefined; ApplyPluginsType = ApplyPluginsType; EnableBy = EnableBy; ConfigChangeType = ConfigChangeType; ServiceStage = ServiceStage; args: any; constructor(opts: IServiceOpts) { super(); this.cwd = opts.cwd || process.cwd(); // 仓库根目录,antd pro构建的时候须要一个新的空文件夹 this.pkg = opts.pkg || this.resolvePackage(); this.env = opts.env || process.env.NODE_ENV; // babel处理 this.babelRegister = new BabelRegister(); // 加载环境变量 this.loadEnv(); // 获取用户配置 this.configInstance = new Config({ cwd: this.cwd, service: this, localConfig: this.env === 'development', }); // 从.umirc.ts中获取内容 this.userConfig = this.configInstance.getUserConfig(); // 获取导出的配置 this.paths = getPaths({ cwd: this.cwd, config: this.userConfig!, env: this.env, }); // 初始化插件 const baseOpts = { pkg: this.pkg, cwd: this.cwd, }; // 初始化预设 this.initialPresets = resolvePresets({ ...baseOpts, presets: opts.presets || [], userConfigPresets: this.userConfig.presets || [], }); // 初始化插件 this.initialPlugins = resolvePlugins({ ...baseOpts, plugins: opts.plugins || [], userConfigPlugins: this.userConfig.plugins || [], }); // 初始化配置及插件放入babel注册中 this.babelRegister.setOnlyMap({ key: 'initialPlugins', value: lodash.uniq([ ...this.initialPresets.map(({ path }) => path), ...this.initialPlugins.map(({ path }) => path), ]), }); } // 设置生命周期 setStage(stage: ServiceStage) { this.stage = stage; } // 解析package.json的文件 resolvePackage() { try { return require(join(this.cwd, 'package.json')); } catch (e) { return {}; } } // 加载环境 loadEnv() { const basePath = join(this.cwd, '.env'); const localPath = `${basePath}.local`; loadDotEnv(basePath); loadDotEnv(localPath); } // 真正的初始化 async init() { this.setStage(ServiceStage.init); await this.initPresetsAndPlugins(); // 状态:初始 this.setStage(ServiceStage.initHooks); // 注册了plugin要执行的钩子方法 Object.keys(this.hooksByPluginId).forEach((id) => { const hooks = this.hooksByPluginId[id]; hooks.forEach((hook) => { const { key } = hook; hook.pluginId = id; this.hooks[key] = (this.hooks[key] || []).concat(hook); }); }); // 状态:插件已注册 this.setStage(ServiceStage.pluginReady); // 执行插件 await this.applyPlugins({ key: 'onPluginReady', type: ApplyPluginsType.event, }); // 状态:获取配置信息 this.setStage(ServiceStage.getConfig); // 拿到对应插件的默认配置信息 const defaultConfig = await this.applyPlugins({ key: 'modifyDefaultConfig', type: this.ApplyPluginsType.modify, initialValue: await this.configInstance.getDefaultConfig(), }); // 将实例中的配置信息对应修改的配置信息 this.config = await this.applyPlugins({ key: 'modifyConfig', type: this.ApplyPluginsType.modify, initialValue: this.configInstance.getConfig({ defaultConfig, }) as any, }); // 状态:合并路径 this.setStage(ServiceStage.getPaths); if (this.config!.outputPath) { this.paths.absOutputPath = join(this.cwd, this.config!.outputPath); } // 修改路径对象 const paths = (await this.applyPlugins({ key: 'modifyPaths', type: ApplyPluginsType.modify, initialValue: this.paths, })) as object; Object.keys(paths).forEach((key) => { this.paths[key] = paths[key]; }); } async initPresetsAndPlugins() { this.setStage(ServiceStage.initPresets); this._extraPlugins = []; while (this.initialPresets.length) { await this.initPreset(this.initialPresets.shift()!); } this.setStage(ServiceStage.initPlugins); this._extraPlugins.push(...this.initialPlugins); while (this._extraPlugins.length) { await this.initPlugin(this._extraPlugins.shift()!); } } getPluginAPI(opts: any) { const pluginAPI = new PluginAPI(opts); [ 'onPluginReady', 'modifyPaths', 'onStart', 'modifyDefaultConfig', 'modifyConfig', ].forEach((name) => { pluginAPI.registerMethod({ name, exitsError: false }); }); return new Proxy(pluginAPI, { get: (target, prop: string) => { // 因为 pluginMethods 须要在 register 阶段可用 // 必须经过 proxy 的方式动态获取最新,以实现边注册边使用的效果 if (this.pluginMethods[prop]) return this.pluginMethods[prop]; if ( [ 'applyPlugins', 'ApplyPluginsType', 'EnableBy', 'ConfigChangeType', 'babelRegister', 'stage', 'ServiceStage', 'paths', 'cwd', 'pkg', 'userConfig', 'config', 'env', 'args', 'hasPlugins', 'hasPresets', ].includes(prop) ) { return typeof this[prop] === 'function' ? this[prop].bind(this) : this[prop]; } return target[prop]; }, }); } async applyAPI(opts: { apply: Function; api: PluginAPI }) { let ret = opts.apply()(opts.api); if (isPromise(ret)) { ret = await ret; } return ret || {}; } // 初始化配置 async initPreset(preset: IPreset) { const { id, key, apply } = preset; preset.isPreset = true; const api = this.getPluginAPI({ id, key, service: this }); // register before apply this.registerPlugin(preset); // TODO: ...defaultConfigs 考虑要不要支持,可能这个需求能够经过其余渠道实现 const { presets, plugins, ...defaultConfigs } = await this.applyAPI({ api, apply, }); // register extra presets and plugins if (presets) { assert( Array.isArray(presets), `presets returned from preset ${id} must be Array.`, ); // 插到最前面,下个 while 循环优先执行 this._extraPresets.splice( 0, 0, ...presets.map((path: string) => { return pathToObj({ type: PluginType.preset, path, cwd: this.cwd, }); }), ); } // 深度优先 const extraPresets = lodash.clone(this._extraPresets); this._extraPresets = []; while (extraPresets.length) { await this.initPreset(extraPresets.shift()!); } if (plugins) { assert( Array.isArray(plugins), `plugins returned from preset ${id} must be Array.`, ); this._extraPlugins.push( ...plugins.map((path: string) => { return pathToObj({ type: PluginType.plugin, path, cwd: this.cwd, }); }), ); } } // 初始化插件 async initPlugin(plugin: IPlugin) { const { id, key, apply } = plugin; const api = this.getPluginAPI({ id, key, service: this }); // register before apply this.registerPlugin(plugin); await this.applyAPI({ api, apply }); } getPluginOptsWithKey(key: string) { return getUserConfigWithKey({ key, userConfig: this.userConfig, }); } // 注册插件 registerPlugin(plugin: IPlugin) { // 考虑要不要去掉这里的校验逻辑 // 理论上不会走到这里,由于在 describe 的时候已经作了冲突校验 if (this.plugins[plugin.id]) { const name = plugin.isPreset ? 'preset' : 'plugin'; throw new Error(`\ ${name} ${plugin.id} is already registered by ${this.plugins[plugin.id].path}, \ ${name} from ${plugin.path} register failed.`); } this.plugins[plugin.id] = plugin; } isPluginEnable(pluginId: string) { // api.skipPlugins() 的插件 if (this.skipPluginIds.has(pluginId)) return false; const { key, enableBy } = this.plugins[pluginId]; // 手动设置为 false if (this.userConfig[key] === false) return false; // 配置开启 if (enableBy === this.EnableBy.config && !(key in this.userConfig)) { return false; } // 函数自定义开启 if (typeof enableBy === 'function') { return enableBy(); } // 注册开启 return true; } // 判断函数:是否有插件 hasPlugins(pluginIds: string[]) { return pluginIds.every((pluginId) => { const plugin = this.plugins[pluginId]; return plugin && !plugin.isPreset && this.isPluginEnable(pluginId); }); } // 判断函数:是否有预设 hasPresets(presetIds: string[]) { return presetIds.every((presetId) => { const preset = this.plugins[presetId]; return preset && preset.isPreset && this.isPluginEnable(presetId); }); } // 真正的插件执行函数,基于promise实现 async applyPlugins(opts: { key: string; type: ApplyPluginsType; initialValue?: any; args?: any; }) { const hooks = this.hooks[opts.key] || []; switch (opts.type) { case ApplyPluginsType.add: if ('initialValue' in opts) { assert( Array.isArray(opts.initialValue), `applyPlugins failed, opts.initialValue must be Array if opts.type is add.`, ); } const tAdd = new AsyncSeriesWaterfallHook(['memo']); for (const hook of hooks) { if (!this.isPluginEnable(hook.pluginId!)) { continue; } tAdd.tapPromise( { name: hook.pluginId!, stage: hook.stage || 0, // @ts-ignore before: hook.before, }, async (memo: any[]) => { const items = await hook.fn(opts.args); return memo.concat(items); }, ); } return await tAdd.promise(opts.initialValue || []); case ApplyPluginsType.modify: const tModify = new AsyncSeriesWaterfallHook(['memo']); for (const hook of hooks) { if (!this.isPluginEnable(hook.pluginId!)) { continue; } tModify.tapPromise( { name: hook.pluginId!, stage: hook.stage || 0, // @ts-ignore before: hook.before, }, async (memo: any) => { return await hook.fn(memo, opts.args); }, ); } return await tModify.promise(opts.initialValue); case ApplyPluginsType.event: const tEvent = new AsyncSeriesWaterfallHook(['_']); for (const hook of hooks) { if (!this.isPluginEnable(hook.pluginId!)) { continue; } tEvent.tapPromise( { name: hook.pluginId!, stage: hook.stage || 0, // @ts-ignore before: hook.before, }, async () => { await hook.fn(opts.args); }, ); } return await tEvent.promise(); default: throw new Error( `applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`, ); } } // 运行方法 async run({ name, args = {} }: { name: string; args?: any }) { args._ = args._ || []; if (args._[0] === name) args._.shift(); this.args = args; await this.init(); this.setStage(ServiceStage.run); await this.applyPlugins({ key: 'onStart', type: ApplyPluginsType.event, args: { args, }, }); return this.runCommand({ name, args }); } // 运行命令 async runCommand({ name, args = {} }: { name: string; args?: any }) { assert(this.stage >= ServiceStage.init, `service is not initialized.`); args._ = args._ || []; if (args._[0] === name) args._.shift(); const command = typeof this.commands[name] === 'string' ? this.commands[this.commands[name] as string] : this.commands[name]; assert(command, `run command failed, command ${name} does not exists.`); const { fn } = command as ICommand; return fn({ args }); } }
export default function getServicePaths({ cwd, config, env, }: { cwd: string; config: any; env?: string; }): IServicePaths { // 项目根目录 let absSrcPath = cwd; // 若是存在src目录,将absSrcPath定位到src路径下 if (isDirectoryAndExist(join(cwd, 'src'))) { absSrcPath = join(cwd, 'src'); } // src下是page仍是pages const absPagesPath = config.singular ? join(absSrcPath, 'page') : join(absSrcPath, 'pages'); // 临时文件路径 const tmpDir = ['.umi', env !== 'development' && env] .filter(Boolean) .join('-'); // outputPath 指定输出路径 return normalizeWithWinPath({ cwd, absNodeModulesPath: join(cwd, 'node_modules'), absOutputPath: join(cwd, config.outputPath || './dist'), absSrcPath, absPagesPath, absTmpPath: join(absSrcPath, tmpDir), }); }
描述插件核心方法的类,插件的编写需借助这个api,扩展方法须要在preset-built-in的presets集合中进行扩展github
export default class PluginAPI { // 插件的id,区别不一样的插件 id: string; // 插件内的不一样内容,如方法及数据等 key: string; service: Service; Html: typeof Html; utils: typeof utils; logger: Logger; constructor(opts: IOpts) { this.id = opts.id; this.key = opts.key; this.service = opts.service; this.utils = utils; this.Html = Html; this.logger = new Logger(`umi:plugin:${this.id || this.key}`); } // TODO: reversed keys describe({ id, key, config, enableBy, }: { id?: string; key?: string; config?: IPluginConfig; enableBy?: EnableBy | (() => boolean); } = {}) { const { plugins } = this.service; // this.id and this.key is generated automatically // so we need to diff first if (id && this.id !== id) { if (plugins[id]) { const name = plugins[id].isPreset ? 'preset' : 'plugin'; throw new Error( `api.describe() failed, ${name} ${id} is already registered by ${plugins[id].path}.`, ); } plugins[id] = plugins[this.id]; plugins[id].id = id; delete plugins[this.id]; this.id = id; } if (key && this.key !== key) { this.key = key; plugins[this.id].key = key; } if (config) { plugins[this.id].config = config; } plugins[this.id].enableBy = enableBy || EnableBy.register; } // 注册插件 register(hook: IHook) { assert( hook.key && typeof hook.key === 'string', `api.register() failed, hook.key must supplied and should be string, but got ${hook.key}.`, ); assert( hook.fn && typeof hook.fn === 'function', `api.register() failed, hook.fn must supplied and should be function, but got ${hook.fn}.`, ); this.service.hooksByPluginId[this.id] = ( this.service.hooksByPluginId[this.id] || [] ).concat(hook); } // 注册插件命令 registerCommand(command: ICommand) { const { name, alias } = command; assert( !this.service.commands[name], `api.registerCommand() failed, the command ${name} is exists.`, ); this.service.commands[name] = command; if (alias) { this.service.commands[alias] = name; } } // 注册预设 registerPresets(presets: (IPreset | string)[]) { assert( this.service.stage === ServiceStage.initPresets, `api.registerPresets() failed, it should only used in presets.`, ); assert( Array.isArray(presets), `api.registerPresets() failed, presets must be Array.`, ); const extraPresets = presets.map((preset) => { return isValidPlugin(preset as any) ? (preset as IPreset) : pathToObj({ type: PluginType.preset, path: preset as string, cwd: this.service.cwd, }); }); // 插到最前面,下个 while 循环优先执行 this.service._extraPresets.splice(0, 0, ...extraPresets); } // 在 preset 初始化阶段放后面,在插件注册阶段放前面 registerPlugins(plugins: (IPlugin | string)[]) { assert( this.service.stage === ServiceStage.initPresets || this.service.stage === ServiceStage.initPlugins, `api.registerPlugins() failed, it should only be used in registering stage.`, ); assert( Array.isArray(plugins), `api.registerPlugins() failed, plugins must be Array.`, ); const extraPlugins = plugins.map((plugin) => { return isValidPlugin(plugin as any) ? (plugin as IPreset) : pathToObj({ type: PluginType.plugin, path: plugin as string, cwd: this.service.cwd, }); }); if (this.service.stage === ServiceStage.initPresets) { this.service._extraPlugins.push(...extraPlugins); } else { this.service._extraPlugins.splice(0, 0, ...extraPlugins); } } // 注册方法 registerMethod({ name, fn, exitsError = true, }: { name: string; fn?: Function; exitsError?: boolean; }) { if (this.service.pluginMethods[name]) { if (exitsError) { throw new Error( `api.registerMethod() failed, method ${name} is already exist.`, ); } else { return; } } this.service.pluginMethods[name] = fn || // 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI // 不然 pluginId 会不会,致使不能正确 skip plugin function (fn: Function) { const hook = { key: name, ...(utils.lodash.isPlainObject(fn) ? fn : { fn }), }; // @ts-ignore this.register(hook); }; } // 跳过插件,不执行的插件 skipPlugins(pluginIds: string[]) { pluginIds.forEach((pluginId) => { this.service.skipPluginIds.add(pluginId); }); } }
文件名 | 做用 | 备注 |
---|---|---|
Route.ts | 路由的核心类 | 封装了路由匹配等方法 |
routesToJSON.ts | 路由转化为json的方法 | 用于先后端传递 |
getConventionalRoutes.ts | 获取默认路由 | 前端开发时经常使用写的路由表 |
class Route { opts: IOpts; constructor(opts?: IOpts) { this.opts = opts || {}; } async getRoutes(opts: IGetRoutesOpts) { // config 用户 + 插件配置 // root 是 absPagesPath // componentPrefix是路径的分割符号,默认是 "/" const { config, root, componentPrefix } = opts; // 避免修改配置里的 routes,致使重复 patch let routes = lodash.cloneDeep(config.routes); let isConventional = false; // 若是用户没有自定义,则使用约定式路由;若是配置了则约定式路由无效 if (!routes) { assert(root, `opts.root must be supplied for conventional routes.`); // 默认路由的拼接方式 routes = this.getConventionRoutes({ root: root!, config, componentPrefix, }); isConventional = true; } // 生成的路由能够被插件新增,修改,删除 await this.patchRoutes(routes, { ...opts, isConventional, }); return routes; } // TODO: // 1. 移动 /404 到最后,并处理 component 和 redirect async patchRoutes(routes: IRoute[], opts: IGetRoutesOpts) { // 执行插件的 onPatchRoutesBefore 钩子函数对路由修改 if (this.opts.onPatchRoutesBefore) { await this.opts.onPatchRoutesBefore({ routes, parentRoute: opts.parentRoute, }); } // routes中的route执行patrchRoute方法 for (const route of routes) { await this.patchRoute(route, opts); } // onPatchRoutes进行最终的路由修改 if (this.opts.onPatchRoutes) { await this.opts.onPatchRoutes({ routes, parentRoute: opts.parentRoute, }); } } async patchRoute(route: IRoute, opts: IGetRoutesOpts) { if (this.opts.onPatchRouteBefore) { await this.opts.onPatchRouteBefore({ route, parentRoute: opts.parentRoute, }); } // route.path 的修改须要在子路由 patch 以前作 if ( route.path && route.path.charAt(0) !== '/' && !/^https?:\/\//.test(route.path) ) { route.path = winPath(join(opts.parentRoute?.path || '/', route.path)); } if (route.redirect && route.redirect.charAt(0) !== '/') { route.redirect = winPath( join(opts.parentRoute?.path || '/', route.redirect), ); } // 递归 patchRoutes if (route.routes) { await this.patchRoutes(route.routes, { ...opts, parentRoute: route, }); } else { if (!('exact' in route)) { // exact by default route.exact = true; } } // resolve component path if ( route.component && !opts.isConventional && typeof route.component === 'string' && !route.component.startsWith('@/') && !path.isAbsolute(route.component) ) { route.component = winPath(join(opts.root, route.component)); } // resolve wrappers path if (route.wrappers) { route.wrappers = route.wrappers.map((wrapper) => { if (wrapper.startsWith('@/') || path.isAbsolute(wrapper)) { return wrapper; } else { return winPath(join(opts.root, wrapper)); } }); } // onPatchRoute 钩子函数 if (this.opts.onPatchRoute) { await this.opts.onPatchRoute({ route, parentRoute: opts.parentRoute, }); } } // 约定式路由 getConventionRoutes(opts: any): IRoute[] { return getConventionalRoutes(opts); } getJSON(opts: { routes: IRoute[]; config: IConfig; cwd: string }) { return routesToJSON(opts); } getPaths({ routes }: { routes: IRoute[] }): string[] { return lodash.uniq( routes.reduce((memo: string[], route) => { if (route.path) memo.push(route.path); if (route.routes) memo = memo.concat(this.getPaths({ routes: route.routes })); return memo; }, []), ); } }
// 正则匹配,而后JSON.stringify() export default function ({ routes, config, cwd }: IOpts) { // 由于要往 routes 里加无用的信息,因此必须 deep clone 一下,避免污染 const clonedRoutes = lodash.cloneDeep(routes); if (config.dynamicImport) { patchRoutes(clonedRoutes); } function patchRoutes(routes: IRoute[]) { routes.forEach(patchRoute); } function patchRoute(route: IRoute) { if (route.component && !isFunctionComponent(route.component)) { const webpackChunkName = routeToChunkName({ route, cwd, }); // 解决 SSR 开启动态加载后,页面闪烁问题 if (config?.ssr && config?.dynamicImport) { route._chunkName = webpackChunkName; } route.component = [ route.component, webpackChunkName, route.path || EMPTY_PATH, ].join(SEPARATOR); } if (route.routes) { patchRoutes(route.routes); } } function isFunctionComponent(component: string) { return ( /^\((.+)?\)(\s+)?=>/.test(component) || /^function([^\(]+)?\(([^\)]+)?\)([^{]+)?{/.test(component) ); } function replacer(key: string, value: any) { switch (key) { case 'component': if (isFunctionComponent(value)) return value; if (config.dynamicImport) { const [component, webpackChunkName] = value.split(SEPARATOR); let loading = ''; if (config.dynamicImport.loading) { loading = `, loading: LoadingComponent`; } return `dynamic({ loader: () => import(/* webpackChunkName: '${webpackChunkName}' */'${component}')${loading}})`; } else { return `require('${value}').default`; } case 'wrappers': const wrappers = value.map((wrapper: string) => { if (config.dynamicImport) { let loading = ''; if (config.dynamicImport.loading) { loading = `, loading: LoadingComponent`; } return `dynamic({ loader: () => import(/* webpackChunkName: 'wrappers' */'${wrapper}')${loading}})`; } else { return `require('${wrapper}').default`; } }); return `[${wrappers.join(', ')}]`; default: return value; } } return JSON.stringify(clonedRoutes, replacer, 2) .replace(/\"component\": (\"(.+?)\")/g, (global, m1, m2) => { return `"component": ${m2.replace(/\^/g, '"')}`; }) .replace(/\"wrappers\": (\"(.+?)\")/g, (global, m1, m2) => { return `"wrappers": ${m2.replace(/\^/g, '"')}`; }) .replace(/\\r\\n/g, '\r\n') .replace(/\\n/g, '\r\n'); }
须要考虑多种状况,如:目录、文件、动态路由等web
// 考虑多种状况: // 多是目录,没有后缀,好比 [post]/add.tsx // 多是文件,有后缀,好比 [id].tsx // [id$] 是可选动态路由 const RE_DYNAMIC_ROUTE = /^\[(.+?)\]/; // 获取文件,主要就是fs模块的读写问价等方法 function getFiles(root: string) { if (!existsSync(root)) return []; return readdirSync(root).filter((file) => { const absFile = join(root, file); const fileStat = statSync(absFile); const isDirectory = fileStat.isDirectory(); const isFile = fileStat.isFile(); if ( isDirectory && ['components', 'component', 'utils', 'util'].includes(file) ) { return false; } if (file.charAt(0) === '.') return false; if (file.charAt(0) === '_') return false; // exclude test file if (/\.(test|spec|e2e)\.(j|t)sx?$/.test(file)) return false; // d.ts if (/\.d\.ts$/.test(file)) return false; if (isFile) { if (!/\.(j|t)sx?$/.test(file)) return false; const content = readFileSync(absFile, 'utf-8'); try { if (!isReactComponent(content)) return false; } catch (e) { throw new Error( `Parse conventional route component ${absFile} failed, ${e.message}`, ); } } return true; }); } // 文件路由的reducer方法 function fileToRouteReducer(opts: IOpts, memo: IRoute[], file: string) { const { root, relDir = '' } = opts; const absFile = join(root, relDir, file); const stats = statSync(absFile); const __isDynamic = RE_DYNAMIC_ROUTE.test(file); if (stats.isDirectory()) { const relFile = join(relDir, file); const layoutFile = getFile({ base: join(root, relFile), fileNameWithoutExt: '_layout', type: 'javascript', }); const route = { path: normalizePath(relFile, opts), routes: getRoutes({ ...opts, relDir: join(relFile), }), __isDynamic, ...(layoutFile ? { component: layoutFile.path, } : { exact: true, __toMerge: true, }), }; memo.push(normalizeRoute(route, opts)); } else { const bName = basename(file, extname(file)); memo.push( normalizeRoute( { path: normalizePath(join(relDir, bName), opts), exact: true, component: absFile, __isDynamic, }, opts, ), ); } return memo; } // 格式化路由 function normalizeRoute(route: IRoute, opts: IOpts) { let props: unknown = undefined; if (route.component) { try { props = getExportProps(readFileSync(route.component, 'utf-8')); } catch (e) { throw new Error( `Parse conventional route component ${route.component} failed, ${e.message}`, ); } route.component = winPath(relative(join(opts.root, '..'), route.component)); route.component = `${opts.componentPrefix || '@/'}${route.component}`; } return { ...route, ...(typeof props === 'object' ? props : {}), }; } // 格式化路径 function normalizePath(path: string, opts: IOpts) { path = winPath(path) .split('/') .map((p) => { // dynamic route p = p.replace(RE_DYNAMIC_ROUTE, ':$1'); // :post$ => :post? if (p.endsWith('$')) { p = p.slice(0, -1) + '?'; } return p; }) .join('/'); path = `/${path}`; // /index/index -> / if (path === '/index/index') { path = '/'; } // /xxxx/index -> /xxxx/ path = path.replace(/\/index$/, '/'); // remove the last slash // e.g. /abc/ -> /abc if (path !== '/' && path.slice(-1) === '/') { path = path.slice(0, -1); } return path; } // 格式化路由表 function normalizeRoutes(routes: IRoute[]): IRoute[] { const paramsRoutes: IRoute[] = []; const exactRoutes: IRoute[] = []; const layoutRoutes: IRoute[] = []; routes.forEach((route) => { const { __isDynamic, exact } = route; delete route.__isDynamic; if (__isDynamic) { paramsRoutes.push(route); } else if (exact) { exactRoutes.push(route); } else { layoutRoutes.push(route); } }); assert( paramsRoutes.length <= 1, `We should not have multiple dynamic routes under a directory.`, ); return [...exactRoutes, ...layoutRoutes, ...paramsRoutes].reduce( (memo, route) => { if (route.__toMerge && route.routes) { memo = memo.concat(route.routes); } else { memo.push(route); } return memo; }, [] as IRoute[], ); } // 获取路由表 export default function getRoutes(opts: IOpts) { const { root, relDir = '', config } = opts; const files = getFiles(join(root, relDir)); const routes = normalizeRoutes( files.reduce(fileToRouteReducer.bind(null, opts), []), ); if (!relDir) { const globalLayoutFile = getFile({ base: root, fileNameWithoutExt: `../${config.singular ? 'layout' : 'layouts'}/index`, type: 'javascript', }); if (globalLayoutFile) { return [ normalizeRoute( { path: '/', component: globalLayoutFile.path, routes, }, opts, ), ]; } } return routes; }
文件名 | 做用 | 备注 |
---|---|---|
Config.ts | 核心配置类 | 关联用户输入与脚手架输出的中介者 |
export default class Config { cwd: string; service: Service; config?: object; localConfig?: boolean; configFile?: string | null; constructor(opts: IOpts) { this.cwd = opts.cwd || process.cwd(); this.service = opts.service; this.localConfig = opts.localConfig; } // 获取默认配置 async getDefaultConfig() { const pluginIds = Object.keys(this.service.plugins); // collect default config let defaultConfig = pluginIds.reduce((memo, pluginId) => { const { key, config = {} } = this.service.plugins[pluginId]; if ('default' in config) memo[key] = config.default; return memo; }, {}); return defaultConfig; } // 获取配置的方法 getConfig({ defaultConfig }: { defaultConfig: object }) { assert( this.service.stage >= ServiceStage.pluginReady, `Config.getConfig() failed, it should not be executed before plugin is ready.`, ); const userConfig = this.getUserConfig(); // 用于提示用户哪些 key 是未定义的 // TODO: 考虑不排除 false 的 key const userConfigKeys = Object.keys(userConfig).filter((key) => { return userConfig[key] !== false; }); // get config const pluginIds = Object.keys(this.service.plugins); pluginIds.forEach((pluginId) => { const { key, config = {} } = this.service.plugins[pluginId]; // recognize as key if have schema config if (!config.schema) return; const value = getUserConfigWithKey({ key, userConfig }); // 不校验 false 的值,此时已禁用插件 if (value === false) return; // do validate const schema = config.schema(joi); assert( joi.isSchema(schema), `schema return from plugin ${pluginId} is not valid schema.`, ); const { error } = schema.validate(value); if (error) { const e = new Error( `Validate config "${key}" failed, ${error.message}`, ); e.stack = error.stack; throw e; } // remove key const index = userConfigKeys.indexOf(key.split('.')[0]); if (index !== -1) { userConfigKeys.splice(index, 1); } // update userConfig with defaultConfig if (key in defaultConfig) { const newValue = mergeDefault({ defaultConfig: defaultConfig[key], config: value, }); updateUserConfigWithKey({ key, value: newValue, userConfig, }); } }); if (userConfigKeys.length) { const keys = userConfigKeys.length > 1 ? 'keys' : 'key'; throw new Error(`Invalid config ${keys}: ${userConfigKeys.join(', ')}`); } return userConfig; } // 获取用户配置 getUserConfig() { const configFile = this.getConfigFile(); this.configFile = configFile; // 潜在问题: // .local 和 .env 的配置必须有 configFile 才有效 if (configFile) { let envConfigFile; if (process.env.UMI_ENV) { const envConfigFileName = this.addAffix( configFile, process.env.UMI_ENV, ); const fileNameWithoutExt = envConfigFileName.replace( extname(envConfigFileName), '', ); envConfigFile = getFile({ base: this.cwd, fileNameWithoutExt, type: 'javascript', })?.filename; if (!envConfigFile) { throw new Error( `get user config failed, ${envConfigFile} does not exist, but process.env.UMI_ENV is set to ${process.env.UMI_ENV}.`, ); } } const files = [ configFile, envConfigFile, this.localConfig && this.addAffix(configFile, 'local'), ] .filter((f): f is string => !!f) .map((f) => join(this.cwd, f)) .filter((f) => existsSync(f)); // clear require cache and set babel register const requireDeps = files.reduce((memo: string[], file) => { memo = memo.concat(parseRequireDeps(file)); return memo; }, []); requireDeps.forEach(cleanRequireCache); this.service.babelRegister.setOnlyMap({ key: 'config', value: requireDeps, }); // require config and merge return this.mergeConfig(...this.requireConfigs(files)); } else { return {}; } } addAffix(file: string, affix: string) { const ext = extname(file); return file.replace(new RegExp(`${ext}$`), `.${affix}${ext}`); } requireConfigs(configFiles: string[]) { return configFiles.map((f) => compatESModuleRequire(require(f))); } mergeConfig(...configs: object[]) { let ret = {}; for (const config of configs) { // TODO: 精细化处理,好比处理 dotted config key ret = deepmerge(ret, config); } return ret; } getConfigFile(): string | null { // TODO: support custom config file const configFile = CONFIG_FILES.find((f) => existsSync(join(this.cwd, f))); return configFile ? winPath(configFile) : null; } getWatchFilesAndDirectories() { const umiEnv = process.env.UMI_ENV; const configFiles = lodash.clone(CONFIG_FILES); CONFIG_FILES.forEach((f) => { if (this.localConfig) configFiles.push(this.addAffix(f, 'local')); if (umiEnv) configFiles.push(this.addAffix(f, umiEnv)); }); const configDir = winPath(join(this.cwd, 'config')); const files = configFiles .reduce<string[]>((memo, f) => { const file = winPath(join(this.cwd, f)); if (existsSync(file)) { memo = memo.concat(parseRequireDeps(file)); } else { memo.push(file); } return memo; }, []) .filter((f) => !f.startsWith(configDir)); return [configDir].concat(files); } // 发布订阅,监听用户配置的修改 watch(opts: { userConfig: object; onChange: (args: { userConfig: any; pluginChanged: IChanged[]; valueChanged: IChanged[]; }) => void; }) { let paths = this.getWatchFilesAndDirectories(); let userConfig = opts.userConfig; const watcher = chokidar.watch(paths, { ignoreInitial: true, cwd: this.cwd, }); watcher.on('all', (event, path) => { console.log(chalk.green(`[${event}] ${path}`)); const newPaths = this.getWatchFilesAndDirectories(); const diffs = lodash.difference(newPaths, paths); if (diffs.length) { watcher.add(diffs); paths = paths.concat(diffs); } const newUserConfig = this.getUserConfig(); const pluginChanged: IChanged[] = []; const valueChanged: IChanged[] = []; Object.keys(this.service.plugins).forEach((pluginId) => { const { key, config = {} } = this.service.plugins[pluginId]; // recognize as key if have schema config if (!config.schema) return; if (!isEqual(newUserConfig[key], userConfig[key])) { const changed = { key, pluginId: pluginId, }; if (newUserConfig[key] === false || userConfig[key] === false) { pluginChanged.push(changed); } else { valueChanged.push(changed); } } }); debug(`newUserConfig: ${JSON.stringify(newUserConfig)}`); debug(`oldUserConfig: ${JSON.stringify(userConfig)}`); debug(`pluginChanged: ${JSON.stringify(pluginChanged)}`); debug(`valueChanged: ${JSON.stringify(valueChanged)}`); if (pluginChanged.length || valueChanged.length) { opts.onChange({ userConfig: newUserConfig, pluginChanged, valueChanged, }); } userConfig = newUserConfig; }); return () => { watcher.close(); }; } }
umi是蚂蚁金服前端架构的基石,其余的各类扩展应用,诸如:antd组件库、dva数据流等,都是基于umi来构建的,而Ant Design Pro算是蚂蚁金服中后台应用的一个最佳实践。umi对于自研前端生态的核心基础库有着重要的参考价值,对整个生态的支撑也起着“牵一发而动全身”的做用,若是用一句话来归纳umi的核心设计理念,那就是“约定大于配置”,其余各类设计都是围绕着这一设计哲学展开的,于是对于生态的建设要想好我想给外界传递一种什么样的价值与理念,反复造轮子是没有意义的,只有真正能解决问题,好用的轮子才能走的更长远!