├── juejin ├── pr │ └── README.md ├── cli │ ├── bin │ │ └── yang.js │ ├── src │ │ └── index.ts │ ├── package.json │ ├── package-lock.json │ └── tsconfig.json └── mini-umi │ ├── packages │ ├── packageB │ │ ├── src │ │ │ └── index.ts │ │ ├── .fatherrc.ts │ │ └── package.json │ ├── packageC │ │ ├── src │ │ │ └── index.ts │ │ ├── .fatherrc.ts │ │ └── package.json │ └── packageA │ │ ├── src │ │ └── index.ts │ │ ├── .fatherrc.ts │ │ └── package.json │ ├── pnpm-workspace.yaml │ ├── tsconfig.json │ └── package.json ├── packages ├── core │ ├── src │ │ ├── types.ts │ │ ├── index.ts │ │ ├── constants.ts │ │ ├── hook.ts │ │ ├── servicePlugin.ts │ │ ├── config │ │ │ ├── utils.ts │ │ │ └── config.ts │ │ ├── command.ts │ │ ├── plugin.ts │ │ ├── pluginAPI.ts │ │ └── core.ts │ ├── .fatherrc.ts │ └── package.json ├── umi │ ├── bin │ │ └── mini-umi.js │ ├── .fatherrc.ts │ ├── src │ │ ├── index.ts │ │ ├── userConfig.ts │ │ ├── types.ts │ │ └── cli.ts │ └── package.json ├── preset-umi │ ├── template │ │ ├── app.vue.tpl │ │ ├── main.ts.tpl │ │ ├── routes.ts.tpl │ │ └── index.html.tpl │ ├── .fatherrc.ts │ ├── ssrtemplate │ │ ├── entry-client.js │ │ ├── App.vue │ │ ├── index.html │ │ ├── main.js │ │ ├── router.js.tpl │ │ ├── prerender.js.tpl │ │ └── entry-server.js │ ├── src │ │ ├── types.ts │ │ ├── methods.ts │ │ ├── index.ts │ │ ├── commands │ │ │ ├── preview.ts │ │ │ ├── ssg-preview.ts │ │ │ ├── utils.ts │ │ │ ├── getRoutes.ts │ │ │ ├── build.ts │ │ │ ├── dev.ts │ │ │ ├── ssg.ts │ │ │ └── ssr.ts │ │ └── writeTmpFile.ts │ └── package.json ├── create-m-umi │ ├── templates │ │ ├── docs │ │ │ ├── page-a │ │ │ │ └── son.vue │ │ │ ├── page-a.vue │ │ │ ├── test │ │ │ │ └── testpage.vue │ │ │ └── index.vue │ │ ├── pages │ │ │ ├── page-a │ │ │ │ └── son.vue │ │ │ ├── page-a.vue │ │ │ ├── test │ │ │ │ └── testpage.vue │ │ │ └── index.vue │ │ ├── layout │ │ │ └── index.vue │ │ ├── assets │ │ │ └── mini-umi.jpg │ │ ├── index.d.ts │ │ ├── package.json.tpl │ │ ├── tsconfig.json │ │ ├── mumirc.ts │ │ ├── README.md │ │ ├── plugin.ts │ │ └── localUserRoute.vue │ ├── bin │ │ └── create-mumi.js │ ├── .fatherrc.ts │ ├── package.json │ └── src │ │ └── cli.ts └── preset-example │ ├── .fatherrc.ts │ ├── src │ ├── index.ts │ ├── build.ts │ └── dev.ts │ └── package.json ├── examples └── vue3 │ ├── .gitignore │ ├── docs │ ├── page-a │ │ └── son.vue │ ├── test │ │ └── testpage.vue │ ├── page-a.vue │ └── index.vue │ ├── pages │ ├── page-a │ │ └── son.vue │ ├── page-a.vue │ ├── test │ │ └── testpage.vue │ └── index.vue │ ├── layout │ └── index.vue │ ├── assets │ └── mini-umi.jpg │ ├── index.d.ts │ ├── package.json │ ├── tsconfig.json │ ├── README.md │ ├── mumirc.ts │ ├── plugin.ts │ └── localUserRoute.vue ├── pnpm-workspace.yaml ├── .gitignore ├── .fatherrc.base.ts ├── tsconfig.json ├── turbo.json ├── package.json ├── .github └── workflows │ └── ci.yaml ├── 手写可插拔前端框架小册 ├── 0.小册简介-手写可插拔前端框架Umi.md ├── 技术收敛-什么是微内核架构.md ├── 1-1.前端漫谈-npm包与框架.md ├── 1-3.构建编译-现代前端构建方案.md ├── 1-2.牛刀小试-实现并发布一个CLI.md ├── 1-5.源码调试-脚本断点调试.md └── 1-4.仓库架构-Monorepo仓库实践.md └── README.md /juejin/pr/README.md: -------------------------------------------------------------------------------- 1 | pr 打卡: (一行一位) 2 | 洋 3 | -------------------------------------------------------------------------------- /juejin/cli/bin/yang.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | console.log('我是洋'); 3 | -------------------------------------------------------------------------------- /packages/core/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface UserConfig{ 2 | 3 | } 4 | -------------------------------------------------------------------------------- /juejin/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export const yang = 'yang' 2 | console.log(yang); 3 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageB/src/index.ts: -------------------------------------------------------------------------------- 1 | export const yang = 'yang from b' 2 | -------------------------------------------------------------------------------- /packages/umi/bin/mini-umi.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import('../dist/cli.js') 4 | -------------------------------------------------------------------------------- /examples/vue3/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .vscode 5 | .mini-umi 6 | -------------------------------------------------------------------------------- /examples/vue3/docs/page-a/son.vue: -------------------------------------------------------------------------------- 1 | 2 | I am page-a son route 3 | 4 | -------------------------------------------------------------------------------- /examples/vue3/pages/page-a/son.vue: -------------------------------------------------------------------------------- 1 | 2 | I am page-a son route 3 | 4 | -------------------------------------------------------------------------------- /juejin/mini-umi/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | 3 | - 'examples/*' 4 | - 'packages/*' 5 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageC/src/index.ts: -------------------------------------------------------------------------------- 1 | export const yang = 'yang' 2 | console.log(yang); 3 | -------------------------------------------------------------------------------- /packages/preset-umi/template/app.vue.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/vue3/layout/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 自定义Layout 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/docs/page-a/son.vue: -------------------------------------------------------------------------------- 1 | 2 | I am page-a son route 3 | 4 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/pages/page-a/son.vue: -------------------------------------------------------------------------------- 1 | 2 | I am page-a son route 3 | 4 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageA/src/index.ts: -------------------------------------------------------------------------------- 1 | import { yang } from 'package-b' 2 | 3 | 4 | console.log(yang); 5 | -------------------------------------------------------------------------------- /examples/vue3/assets/mini-umi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyYangzai/mini-umi/HEAD/examples/vue3/assets/mini-umi.jpg -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in direct subdirs of packages/ 3 | - 'packages/*' 4 | - 'examples/*' 5 | -------------------------------------------------------------------------------- /packages/create-m-umi/bin/create-mumi.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | process.env.FS_LOGGER = 'none'; 4 | require('../dist/cli'); 5 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/layout/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 自定义Layout 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .mini-umi 5 | .turbo 6 | .mini-umi-ssr 7 | auto-imports.d.ts 8 | components.d.ts 9 | -------------------------------------------------------------------------------- /packages/core/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | extends: '../../.fatherrc.base.ts' 5 | }); 6 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/assets/mini-umi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BoyYangzai/mini-umi/HEAD/packages/create-m-umi/templates/assets/mini-umi.jpg -------------------------------------------------------------------------------- /packages/umi/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | extends: '../../.fatherrc.base.ts' 5 | }); 6 | -------------------------------------------------------------------------------- /packages/umi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { UserConfig } from './types' 2 | export { defineMumiConfig } from './userConfig' 3 | export {type IApi } from './types' 4 | -------------------------------------------------------------------------------- /packages/create-m-umi/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | extends: '../../.fatherrc.base.ts' 5 | }); 6 | -------------------------------------------------------------------------------- /packages/preset-example/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | extends: '../../.fatherrc.base.ts' 5 | }); 6 | -------------------------------------------------------------------------------- /packages/preset-umi/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | extends: '../../.fatherrc.base.ts', 5 | }); 6 | -------------------------------------------------------------------------------- /packages/umi/src/userConfig.ts: -------------------------------------------------------------------------------- 1 | import { UserConfig } from "./types"; 2 | 3 | export const defineMumiConfig:(config: UserConfig)=>UserConfig = (config) => config 4 | -------------------------------------------------------------------------------- /.fatherrc.base.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | cjs: { 5 | output: "dist", 6 | sourcemap: true 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageA/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "father"; 2 | 3 | export default defineConfig({ 4 | extends:'../../.fatherrc.base.ts' 5 | }) 6 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageB/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "father"; 2 | 3 | export default defineConfig({ 4 | extends: '../../.fatherrc.base.ts' 5 | }) 6 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageC/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "father"; 2 | 3 | export default defineConfig({ 4 | extends: '../../.fatherrc.base.ts' 5 | }) 6 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Core } from './core' 2 | import { PluginAPi } from './pluginAPI' 3 | 4 | export * from './core' 5 | export type ICoreApi = PluginAPi & Core 6 | -------------------------------------------------------------------------------- /examples/vue3/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue' 3 | const Component: ReturnType 4 | export default Component 5 | } 6 | -------------------------------------------------------------------------------- /packages/preset-example/src/index.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return { 3 | plugins: [ 4 | require.resolve('./dev'), 5 | require.resolve('./build') 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue' 3 | const Component: ReturnType 4 | export default Component 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "skipLibCheck": true, 6 | "baseUrl": "./", 7 | "moduleDetection": "auto", 8 | "esModuleInterop": true, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const SHORT_ENV = { 2 | development: 'dev', 3 | production: 'prod', 4 | test: 'test', 5 | }; 6 | 7 | export const LOCAL_EXT = '.local'; 8 | export const DEFAULT_CONFIG_FILES = ['config.ts', 'config.js']; 9 | -------------------------------------------------------------------------------- /packages/preset-umi/template/main.ts.tpl: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './routes'; 4 | import 'element-plus/dist/index.css' 5 | 6 | createApp(App) 7 | .use(router) 8 | .mount('#app'); 9 | -------------------------------------------------------------------------------- /juejin/mini-umi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "declaration": true, 5 | "skipLibCheck": true, 6 | "baseUrl": "./", 7 | "moduleDetection": "auto", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/hook.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "./plugin"; 2 | 3 | export class Hook{ 4 | fn = Function; 5 | plugin = {} as Plugin 6 | constructor({ key, plugin, fn }: { key: string, plugin: Plugin, fn: any }) { 7 | this.plugin = plugin 8 | this.fn = fn 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/preset-umi/ssrtemplate/entry-client.js: -------------------------------------------------------------------------------- 1 | import { createApp } from './main' 2 | 3 | const { app, router } = createApp() 4 | 5 | // wait until router is ready before mounting to ensure hydration match 6 | router.isReady().then(() => { 7 | app.mount('#app') 8 | 9 | console.log('hydrated') 10 | }) 11 | -------------------------------------------------------------------------------- /packages/preset-umi/ssrtemplate/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | -------------------------------------------------------------------------------- /packages/preset-umi/src/types.ts: -------------------------------------------------------------------------------- 1 | import { IWriteTmpFile } from "./writeTmpFile" 2 | import { type UserConfig as ViteUserConfig } from 'vite' 3 | 4 | 5 | export type IpresetUmi = { 6 | writeTmpFile: (opts: IWriteTmpFile) => {}, 7 | modifyViteConfig: (fn: (memo: ViteUserConfig) => ViteUserConfig) => any, 8 | 9 | } 10 | -------------------------------------------------------------------------------- /packages/preset-umi/template/routes.ts.tpl: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter, 3 | createWebHistory, 4 | RouteRecordRaw 5 | } from 'vue-router'; 6 | 7 | const routes: RouteRecordRaw[] = {{{routes}}}; 8 | 9 | const router = createRouter({ 10 | history: createWebHistory(), 11 | routes 12 | }); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /packages/preset-umi/src/methods.ts: -------------------------------------------------------------------------------- 1 | import { ICoreApi } from '@mini-umi/core' 2 | import { IpresetUmi } from "./types" 3 | 4 | export default (api: ICoreApi & IpresetUmi) => { 5 | [ 6 | 'modifyRoutesDir', 7 | 'modifyViteConfig' 8 | ].forEach(name =>{ 9 | api.registerMethod({ 10 | name 11 | }) 12 | }) 13 | 14 | } 15 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageC/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-c", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "father dev", 8 | "build": "father build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/servicePlugin.ts: -------------------------------------------------------------------------------- 1 | import { PluginAPi } from "./pluginAPI"; 2 | 3 | export default (api: PluginAPi) => { 4 | [ 5 | 'onCheck', 6 | 'onStart', 7 | // todo : 8 | 'modifyAppData', 9 | 'modifyConfig', 10 | 'modifyDefaultConfig', 11 | ].forEach((name) => { 12 | api.registerMethod({ name }); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageB/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-b", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "dev": "father dev", 8 | "build": "father build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": [ 6 | "^build" 7 | ], 8 | "outputs": [ 9 | "dist/**",".mini-umi/dist/**" 10 | ] 11 | } 12 | }, 13 | "globalDependencies": [ 14 | "tsconfig.json", 15 | ".father.base.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /juejin/mini-umi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-umi", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "father dev", 8 | "build:all": "pnpm run -r build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "father": "^4.1.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /juejin/mini-umi/packages/packageA/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package-a", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "father dev", 8 | "build": "father build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "package-b": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/preset-example/src/build.ts: -------------------------------------------------------------------------------- 1 | import { type IApi } from "@mini-umi/core"; 2 | 3 | // 插件 4 | export default (IApi:IApi) => { 5 | IApi.register({ 6 | key: 'onBuildStart', 7 | fn: (name) => { 8 | console.log('onBuildStart plugin-B'); 9 | } 10 | }) 11 | IApi.registerCommand({ 12 | name: 'example-build', 13 | fn: () => { 14 | console.log('building!!'); 15 | } 16 | }) 17 | 18 | } 19 | -------------------------------------------------------------------------------- /juejin/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yang-first-pkg", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "./dist/index.js", 6 | "bin": { 7 | "yang": "./bin/yang.js" 8 | }, 9 | "scripts": { 10 | "dev": "tsc --watch", 11 | "build": "tsc --target es5" 12 | }, 13 | "files": [ 14 | "bin" 15 | ], 16 | "keywords": [], 17 | "author": "洋", 18 | "license": "ISC", 19 | "dependencies": { 20 | "yang-first-pkg": "^1.0.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/config/utils.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, join } from 'path'; 2 | 3 | export function addExt(opts: { file: string; ext: string }) { 4 | const index = opts.file.lastIndexOf('.'); 5 | return `${opts.file.slice(0, index)}${opts.ext}${opts.file.slice(index)}`; 6 | } 7 | 8 | export function getAbsFiles(opts: { files: string[]; cwd: string }) { 9 | return opts.files.map((file) => { 10 | return isAbsolute(file) ? file : join(opts.cwd, file); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /packages/preset-umi/template/index.html.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | mini-umi 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/preset-umi/ssrtemplate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | mini-umi 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/preset-example/src/dev.ts: -------------------------------------------------------------------------------- 1 | import { IApi } from "@mini-umi/core"; 2 | 3 | // 插件 4 | export default (IApi: IApi) => { 5 | IApi.registerCommand({ 6 | name: 'example-dev', 7 | fn: () => { 8 | console.log('devServer run!'); 9 | } 10 | }) 11 | IApi.registerCommand({ 12 | name: 'name', 13 | fn: ({ n }) => { 14 | console.log(`Hello,${n}`); 15 | } 16 | }) 17 | IApi.register({ 18 | key: 'onBuildStart', 19 | fn: (name) => { 20 | console.log('onBuildStart plugin-A'); 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/command.ts: -------------------------------------------------------------------------------- 1 | export type ICommand = { 2 | name: string; 3 | fn: (object:any)=>{}; 4 | } 5 | export class Command{ 6 | name; 7 | description; 8 | options; 9 | details; 10 | fn; 11 | plugin; 12 | configResolveMode; 13 | constructor(opts: any) { 14 | this.name = opts.name; 15 | this.description = opts.description; 16 | this.options = opts.options; 17 | this.details = opts.details; 18 | this.fn = opts.fn; 19 | this.plugin = opts.plugin; 20 | this.configResolveMode = opts.configResolveMode || 'strict'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/preset-umi/src/index.ts: -------------------------------------------------------------------------------- 1 | export { type UserConfig as ViteUserConfig } from 'vite' 2 | export { IpresetUmi } from './types' 3 | 4 | export default () => { 5 | return { 6 | plugins: [ 7 | require.resolve('./methods'), 8 | require.resolve('./writeTmpFile'), 9 | require.resolve('./commands/dev'), 10 | require.resolve('./commands/build'), 11 | require.resolve('./commands/preview'), 12 | require.resolve('./commands/ssr'), 13 | require.resolve('./commands/ssg'), 14 | require.resolve('./commands/ssg-preview') 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/umi/src/types.ts: -------------------------------------------------------------------------------- 1 | import { type ViteUserConfig } from "@mini-umi/preset-umi"; 2 | import { type ICoreApi } from "@mini-umi/core"; 3 | import { IpresetUmi } from "@mini-umi/preset-umi"; 4 | 5 | type routes = { 6 | path: string, 7 | name: string, 8 | component?: any, 9 | children?: routes 10 | }[] 11 | export type UserConfig = { 12 | routes: routes 13 | routesDir?: string 14 | viteConfig: ViteUserConfig 15 | } 16 | 17 | 18 | export type IApi = ICoreApi & IpresetUmi & { 19 | modifyConfig: (( 20 | fn: (memo: UserConfig) => UserConfig 21 | ) => UserConfig | undefined) 22 | } 23 | -------------------------------------------------------------------------------- /packages/preset-umi/ssrtemplate/main.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { createSSRApp } from 'vue' 3 | import App from './App.vue' 4 | import { createRouter } from './router' 5 | import 'element-plus/dist/index.css' 6 | 7 | // SSR requires a fresh app instance per request, therefore we export a function 8 | // that creates a fresh app instance. If using Vuex, we'd also be creating a 9 | // fresh store here. 10 | export function createApp() { 11 | const app = createSSRApp(App) 12 | const pinia = createPinia() 13 | app.use(pinia) 14 | const router = createRouter() 15 | app.use(router) 16 | return { app, router } 17 | } 18 | -------------------------------------------------------------------------------- /examples/vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "mumi dev", 8 | "ssr": "mumi ssr", 9 | "build": "mumi build", 10 | "preview": "mumi preview" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "element-plus": "^2.2.25", 17 | "es-module-lexer": "^1.1.0", 18 | "esbuild": "^0.15.16", 19 | "mini-umi": "workspace: *", 20 | "pinia": "^2.0.27", 21 | "unplugin-auto-import": "^0.12.0", 22 | "unplugin-vue-components": "^0.22.11", 23 | "vue": "^3.2.45", 24 | "vue-router": "^4.1.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/package.json.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{{name}}}", 3 | "version": "1.0.0", 4 | "author": "{{{author}}}", 5 | "description": "{{{description}}}", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "mumi dev", 9 | "build": "mumi build", 10 | "ssr": "mumi ssr", 11 | "preview": "mumi preview" 12 | }, 13 | "keywords": ["umijs","mumi","Vue3.2"], 14 | "license": "ISC", 15 | "dependencies": { 16 | "element-plus": "^2.2.25", 17 | "mini-umi": "^1.0.6", 18 | "pinia": "^2.0.27", 19 | "unplugin-auto-import": "^0.12.0", 20 | "unplugin-vue-components": "^0.22.11", 21 | "vue": "^3.2.45", 22 | "vue-router": "^4.1.6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/preset-umi/ssrtemplate/router.js.tpl: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter as _createRouter, 3 | createMemoryHistory, 4 | createWebHistory 5 | } from 'vue-router' 6 | 7 | // Auto generates routes from vue files under ./pages 8 | // https://vitejs.dev/guide/features.html#glob-import 9 | const pages = import.meta.glob('../../src/pages/*.vue') 10 | 11 | const routes = {{{routes}}} 12 | 13 | export function createRouter() { 14 | return _createRouter({ 15 | // use appropriate history implementation for server/client 16 | // import.meta.env.SSR is injected by Vite. 17 | history: import.meta.env.SSR 18 | ? createMemoryHistory() 19 | : createWebHistory(), 20 | routes 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/preview.ts: -------------------------------------------------------------------------------- 1 | import { type ICoreApi } from '@mini-umi/core' 2 | import { IpresetUmi } from '../types'; 3 | import { createServer } from 'vite' 4 | import { join } from 'path' 5 | export default (api: IpresetUmi & ICoreApi) => { 6 | const cwd = process.cwd() 7 | api.registerCommand({ 8 | name: 'preview', 9 | async fn() { 10 | // start server 11 | const server = await createServer({ 12 | // 任何合法的用户配置选项,加上 `mode` 和 `configFile` 13 | configFile: false, 14 | root: join(cwd, './.mini-umi/dist'), 15 | server: { 16 | port: 8000 17 | } 18 | }) 19 | await server.listen() 20 | server.printUrls() 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/preset-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mini-umi/preset-example", 3 | "version": "1.3.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "dev": "father dev", 8 | "build": "father build", 9 | "publish:all": "pnpm publish --no-git-checks", 10 | "build:deps": "father prebundle", 11 | "prepublishOnly": "father doctor && npm run build" 12 | }, 13 | "dependencies": { 14 | }, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "repository": "https://github.com/BoyYangzai/mini-umi", 22 | "keywords": [ 23 | "umi", 24 | "umijs", 25 | "mini-umi" 26 | ], 27 | "author": "洋", 28 | "license": "ISC" 29 | } 30 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/ssg-preview.ts: -------------------------------------------------------------------------------- 1 | import { type ICoreApi } from '@mini-umi/core' 2 | import { IpresetUmi } from '../types'; 3 | import { createServer } from 'vite' 4 | import { join } from 'path' 5 | export default (api: IpresetUmi & ICoreApi) => { 6 | const cwd = process.cwd() 7 | api.registerCommand({ 8 | name: 'ssg-preview', 9 | async fn() { 10 | // start server 11 | const server = await createServer({ 12 | // 任何合法的用户配置选项,加上 `mode` 和 `configFile` 13 | configFile: false, 14 | root: join(cwd, './.mini-umi-ssg/static'), 15 | server: { 16 | port: 8000 17 | } 18 | }) 19 | await server.listen() 20 | server.printUrls() 21 | } 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /packages/preset-umi/src/writeTmpFile.ts: -------------------------------------------------------------------------------- 1 | import { BaseGenerator } from '@umijs/utils' 2 | import { IpresetUmi } from "./types"; 3 | import { join } from 'path' 4 | import { 5 | winPath, 6 | } from '@umijs/utils'; 7 | export interface IWriteTmpFile { 8 | path: string 9 | target: string 10 | data: object 11 | } 12 | export default (api: IpresetUmi) => { 13 | api.registerMethod({ 14 | name: 'writeTmpFile', 15 | async fn(opts: IWriteTmpFile) { 16 | const generate = new BaseGenerator({ 17 | path: winPath(join(join(__dirname, '../template'), opts.path)), 18 | target: opts.target, 19 | data: opts.data, 20 | questions: [] 21 | }) 22 | await generate.run() 23 | } 24 | }) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /examples/vue3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "esModuleInterop": true, 13 | "lib": [ 14 | "esnext", 15 | "dom" 16 | ], 17 | "baseUrl": ".", // 用于设置解析非相对模块名称的基本目录,相对模块不会受到baseUrl的影响 18 | "paths": { // 用于设置模块名到基于baseUrl的路径映射 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | } 23 | }, 24 | "include": [ 25 | "src/**/*.ts", 26 | "src/**/*.d.ts", 27 | "src/**/*.tsx", 28 | "index.d.ts", 29 | "src/env.d.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mini-umi/core", 3 | "version": "1.3.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "dev": "father dev", 8 | "build": "father build", 9 | "publish:all": "pnpm publish --no-git-checks", 10 | "build:deps": "father prebundle", 11 | "prepublishOnly": "father doctor && npm run build" 12 | }, 13 | "dependencies": { 14 | "@umijs/bundler-utils": "^4.0.3", 15 | "@umijs/utils": "^4.0.3" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "repository": "https://github.com/BoyYangzai/mini-umi", 21 | "files": [ 22 | "dist" 23 | ], 24 | "keywords": [ 25 | "umi", 26 | "umijs", 27 | "mini-umi" 28 | ], 29 | "author": "洋", 30 | "license": "ISC" 31 | } 32 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "suppressImplicitAnyIndexErrors": true, 12 | "esModuleInterop": true, 13 | "lib": [ 14 | "esnext", 15 | "dom" 16 | ], 17 | "baseUrl": ".", // 用于设置解析非相对模块名称的基本目录,相对模块不会受到baseUrl的影响 18 | "paths": { // 用于设置模块名到基于baseUrl的路径映射 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | } 23 | }, 24 | "include": [ 25 | "src/**/*.ts", 26 | "src/**/*.d.ts", 27 | "src/**/*.tsx", 28 | "index.d.ts", 29 | "src/env.d.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/create-m-umi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-mumi", 3 | "version": "1.3.7", 4 | "description": "", 5 | "module": "nodenext", 6 | "main": "dist/index.js", 7 | "bin": { 8 | "create": "./bin/create-mumi.js" 9 | }, 10 | "scripts": { 11 | "dev": "father dev", 12 | "build": "father build", 13 | "publish:all": "pnpm publish --no-git-checks", 14 | "build:deps": "father prebundle", 15 | "prepublishOnly": "father doctor && npm run build" 16 | }, 17 | "dependencies": { 18 | "@umijs/utils": "^4.0.33" 19 | }, 20 | "files": [ 21 | "dist", 22 | "templates" 23 | ], 24 | "repository": "https://github.com/BoyYangzai/mini-umi", 25 | "keywords": [ 26 | "umi", 27 | "umijs", 28 | "mini-umi" 29 | ], 30 | "author": "洋", 31 | "license": "ISC" 32 | } 33 | -------------------------------------------------------------------------------- /packages/umi/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { Core } from "@mini-umi/core"; 2 | import yParse from '@umijs/utils/compiled/yargs-parser' 3 | import { existsSync } from 'fs' 4 | import { join } from 'path' 5 | 6 | const cwd = process.cwd() 7 | 8 | const core = new Core({ 9 | cwd: process.cwd(), 10 | env: 'development', 11 | presets: [require.resolve('@mini-umi/preset-example'), require.resolve('@mini-umi/preset-umi')], 12 | plugins: [ 13 | existsSync(join(cwd, 'plugin.ts')) && join(cwd, 'plugin.ts'), 14 | existsSync(join(cwd, 'plugin.js')) && join(cwd, 'plugin.js'), 15 | ].filter(Boolean), 16 | defaultConfigFiles:['mumirc.ts','mumirc.js'] 17 | }) 18 | 19 | const args = yParse(process.argv.slice(2)) 20 | 21 | const currentCommand = args._[0] 22 | const restArgs = { ...args } 23 | 24 | core.run({name: currentCommand,args: restArgs}) 25 | 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-umi~", 3 | "version": "0.0.1", 4 | "description": "a mini system service core", 5 | "main": "dist/cjs/index.js", 6 | "types": "dist/cjs/index.d.ts", 7 | "bin": { 8 | "service-core": "./bin/service.js" 9 | }, 10 | "scripts": { 11 | "build": "turbo run build ", 12 | "publish:all": "pnpm run -r publish:all", 13 | "build:deps": "father prebundle", 14 | "prepublishOnly": "father doctor && npm run build", 15 | "buildExample": " cd examples/vue3 && npm run build" 16 | }, 17 | "keywords": [], 18 | "authors": [ 19 | "洋" 20 | ], 21 | "license": "MIT", 22 | "files": [ 23 | "dist", 24 | "compiled" 25 | ], 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "devDependencies": { 30 | "father": "^4.0.7", 31 | "turbo": "^1.6.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function getRoutesString(routes) { 3 | let str = '[\n' 4 | function dps(routes) { 5 | let t = '' 6 | routes.forEach(item => { 7 | t += '{' 8 | for (const [key, value] of Object.entries(item)) { 9 | t += key + ': ' 10 | if (typeof value !== 'string' && Array.isArray(value)) { 11 | t += `[${dps(value)}]` 12 | } else if (typeof value !== 'string' && getType(value).includes("Function")) { 13 | t += `${value},\n` 14 | } else { 15 | t += `'${value}',\n` 16 | } 17 | } 18 | t += '},' 19 | }) 20 | return t 21 | } 22 | str += dps(routes) 23 | str += ']' 24 | function getType(target) { 25 | return Object.prototype.toString.call(target) 26 | } 27 | 28 | return str 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/vue3/README.md: -------------------------------------------------------------------------------- 1 | ### 微内核架构: 2 | 使用插件扩展你可以想象到的一切 3 | 4 | > ✅ Monorepo 最佳实践 + Turbo 远端构建缓存 5 | ## 体验 mini-umi + Vue3.2 + Vite预设 6 | ``` 7 | npx create-mumi 项目名 8 | npm install 9 | npm run dev 10 | ``` 11 | ## 实现 Service Core 架构的最简模型 12 | 😚已完成: 13 | 14 | ✅ 实现内置 presets plugin 功能 15 | 16 | ✅ 实现 mini-umi 的 command 系统 17 | 18 | ✅ 实现编译时 Hook 19 | 20 | ✅ feat: 读取用户 Local Plugin 21 | 22 | ✅ 完善插件API 23 | 24 | ✅ 实现 create-mumi 脚手架 25 | 26 | ✅ 实现 userConfig 以及 modify 全流程 27 | 28 | ✅ 实现 Preset-Vue3.2 + Vite + dev build preview 29 | 30 | ✅ 实现约定式路由 支持动态路由 Vite 支持 Vue3.2 Hmr 31 | 32 | ✅ 支持对于 dev 约定式路由 的热更新 33 | 34 | ✅ 支持以 LocalPlugin 的形式 ModifyBundleConfig 35 | 36 | ✅ 支持用户配置文件自定义路由 和 更改约定式路由所在目录 37 | 38 | 🤔TODO: 39 | - [ ] 实现自定义 Layout 以及 addLayout api 40 | ... 41 | 42 | 🤔More: 43 | - [ ] 实现一套 preset-react 44 | - [ ] 实现一套 preset-qiankun 45 | - [ ] 实现 father 46 | - [ ] 实现 dumi 47 | 48 | -------------------------------------------------------------------------------- /examples/vue3/mumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineMumiConfig } from "mini-umi"; 2 | import AutoImport from 'unplugin-auto-import/vite'; 3 | import Components from 'unplugin-vue-components/vite'; 4 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; 5 | 6 | export default defineMumiConfig({ 7 | routes: [{ 8 | path: '/localUserRoute', 9 | name: 'localUserRoute', 10 | component: () => import('./localUserRoute.vue'), 11 | }], 12 | // 约定式路由存放目录 如有需求可更改为 docs 或其他 13 | // 这里生效的是 pages 目录,在插件里被拦截更改了 14 | routesDir: './docs', 15 | // 请移步 Vite配置: https://vitejs.dev/config/ 16 | viteConfig: { 17 | plugins: [ 18 | // TODO: Autoimport 在 CSR没问题 在SSR会丢失CSS 19 | // AutoImport({ 20 | // resolvers: [ElementPlusResolver()] 21 | // }), 22 | Components({ 23 | resolvers: [ElementPlusResolver()] 24 | }) 25 | ], 26 | resolve: { 27 | alias: { 28 | }, 29 | } 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/mumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineMumiConfig } from "mini-umi"; 2 | import AutoImport from 'unplugin-auto-import/vite'; 3 | import Components from 'unplugin-vue-components/vite'; 4 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; 5 | 6 | export default defineMumiConfig({ 7 | routes: [{ 8 | path: '/localUserRoute', 9 | name: 'localUserRoute', 10 | component: () => import('../localUserRoute.vue'), 11 | }], 12 | // 约定式路由存放目录 如有需求可更改为 docs 或其他 13 | // 这里生效的是 pages 目录,在插件里被拦截更改了 14 | routesDir: './docs', 15 | // 请移步 Vite配置: https://vitejs.dev/config/ 16 | viteConfig: { 17 | plugins: [ 18 | // Tip: SSR 模式请手动注释 并加上 EP 的 CSS 文件 19 | AutoImport({ 20 | resolvers: [ElementPlusResolver()] 21 | }), 22 | Components({ 23 | resolvers: [ElementPlusResolver()] 24 | }) 25 | ], 26 | resolve: { 27 | alias: { 28 | }, 29 | } 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /packages/umi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-umi", 3 | "version": "1.3.5", 4 | "description": "a simple model for Umi and Vue3.2 + Vite", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "bin": { 8 | "mumi": "bin/mini-umi.js" 9 | }, 10 | "scripts": { 11 | "dev": "father dev", 12 | "build": "father build", 13 | "publish:all": "pnpm publish --no-git-checks", 14 | "build:deps": "father prebundle", 15 | "prepublishOnly": "father doctor && npm run build" 16 | }, 17 | "dependencies": { 18 | "@mini-umi/preset-example": "workspace:*", 19 | "@mini-umi/preset-umi": "workspace:*", 20 | "@mini-umi/core": "workspace:*", 21 | "@umijs/utils": "^4.0.3" 22 | }, 23 | "files": [ 24 | "bin", 25 | "dist" 26 | ], 27 | "repository": "https://github.com/BoyYangzai/mini-umi", 28 | "keywords": [ 29 | "umi", 30 | "umijs", 31 | "mini-umi" 32 | ], 33 | "author": "洋", 34 | "license": "ISC" 35 | } 36 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/README.md: -------------------------------------------------------------------------------- 1 | ### 微内核架构: 2 | 使用插件扩展你可以想象到的一切 3 | 4 | > ✅ Monorepo 最佳实践 + Turbo 远端构建缓存 5 | ## 体验 mini-umi + Vue3.2 + Vite预设 6 | ``` 7 | npx create-mumi 项目名 8 | npm install 9 | npm run dev 10 | npm run ssr // 服务端渲染模式 11 | ``` 12 | ## 实现 Service Core 架构的最简模型 13 | 😚已完成: 14 | 15 | ✅ 实现内置 presets plugin 功能 16 | 17 | ✅ 实现 mini-umi 的 command 系统 18 | 19 | ✅ 实现编译时 Hook 20 | 21 | ✅ feat: 读取用户 Local Plugin 22 | 23 | ✅ 完善插件API 24 | 25 | ✅ 实现 create-mumi 脚手架 26 | 27 | ✅ 实现 userConfig 以及 modify 全流程 28 | 29 | ✅ 实现 Preset-Vue3.2 + Vite + dev build preview 30 | 31 | ✅ 实现约定式路由 支持动态路由 Vite 支持 Vue3.2 Hmr 32 | 33 | ✅ 支持对于 dev 约定式路由 的热更新 34 | 35 | ✅ 支持以 LocalPlugin 的形式 ModifyBundleConfig 36 | 37 | ✅ 支持用户配置文件自定义路由 和 更改约定式路由所在目录 38 | 39 | ✅ 可通过 npm run ssr 开启服务端渲染模式 40 | 41 | ✅ CSR SSR 支持用户自定义Layout 42 | 43 | 🤔TODO: 44 | - [ ] 实现自定义 Layout 以及 addLayout api 45 | ... 46 | 47 | 🤔More: 48 | - [ ] 实现一套 preset-react 49 | - [ ] 实现一套 preset-qiankun 50 | - [ ] 实现 father 51 | - [ ] 实现 dumi 52 | 53 | -------------------------------------------------------------------------------- /examples/vue3/plugin.ts: -------------------------------------------------------------------------------- 1 | import { type IApi } from "mini-umi" 2 | 3 | export default (api: IApi) => { 4 | /** 5 | * 这里是示例生命周期,生命周期可以无限扩展... 6 | */ 7 | api.register({ 8 | key: 'onStart', 9 | fn: () => { 10 | console.log('本地Plugin onStart!'); 11 | } 12 | }) 13 | api.register({ 14 | key: 'onBuildStart', 15 | fn: () => { 16 | console.log('BuildStart------------'); 17 | } 18 | }) 19 | 20 | /** 21 | * 自定义 cli 指令 22 | * 执行 npx mini-umi test 试试 23 | */ 24 | api.registerCommand({ 25 | name: 'test', 26 | fn() { 27 | console.log('test Command 正在执行----'); 28 | } 29 | }) 30 | 31 | /** 32 | * modify 类 hook 务必有返回值,作为下一个 hook 的参数 33 | */ 34 | api.modifyRoutesDir(memo => { 35 | memo = './pages' 36 | return memo 37 | }) 38 | /** 39 | * 修改配置文件中的 viteConfig 40 | * 亦或者直接使用 api.modifyConfig 修改全部配置 41 | */ 42 | api.modifyViteConfig(memo => { 43 | memo.resolve!.alias = { 44 | '@': '../' 45 | } 46 | return memo 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/plugin.ts: -------------------------------------------------------------------------------- 1 | import { type IApi } from "mini-umi" 2 | 3 | export default (api: IApi) => { 4 | /** 5 | * 这里是示例生命周期,生命周期可以无限扩展... 6 | */ 7 | api.register({ 8 | key: 'onStart', 9 | fn: () => { 10 | console.log('本地Plugin onStart!'); 11 | } 12 | }) 13 | api.register({ 14 | key: 'onBuildStart', 15 | fn: () => { 16 | console.log('BuildStart------------'); 17 | } 18 | }) 19 | 20 | /** 21 | * 自定义 cli 指令 22 | * 执行 npx mini-umi test 试试 23 | */ 24 | api.registerCommand({ 25 | name: 'test', 26 | fn() { 27 | console.log('test Command 正在执行----'); 28 | } 29 | }) 30 | 31 | /** 32 | * modify 类 hook 务必有返回值,作为下一个 hook 的参数 33 | */ 34 | api.modifyConfig(memo => { 35 | // memo.viteConfig.xxx == xxx 36 | return memo 37 | }) 38 | /** 39 | * 修改配置文件中的 viteConfig 40 | * 亦或者直接使用 api.modifyConfig 修改全部配置 41 | */ 42 | api.modifyViteConfig(memo => { 43 | memo.resolve!.alias = { 44 | '@': '../' 45 | } 46 | return memo 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /packages/create-m-umi/src/cli.ts: -------------------------------------------------------------------------------- 1 | import { BaseGenerator, prompts, yParser } from '@umijs/utils'; 2 | import { join } from 'path'; 3 | 4 | const args = yParser(process.argv.slice(2), { 5 | alias: { 6 | version: ['v'], 7 | help: ['h'], 8 | }, 9 | boolean: ['version'], 10 | }); 11 | 12 | const cwd = process.cwd(); 13 | const [name] = args._; 14 | 15 | const target = name ? join(cwd, name) : cwd; 16 | 17 | const questions: prompts.PromptObject[] = [ 18 | { 19 | name: 'name', 20 | type: 'text', 21 | message: `Input NPM package name (eg: mumi-vue)`, 22 | }, 23 | { 24 | name: 'description', 25 | type: 'text', 26 | message: `Input project description`, 27 | }, 28 | { 29 | name: 'author', 30 | type: 'text', 31 | message: `Input project author (Name )`, 32 | }, 33 | ]; 34 | 35 | const generator = new BaseGenerator({ 36 | path: join(__dirname, `../templates`), 37 | target, 38 | data: { 39 | version: '^1.0.0', 40 | }, 41 | questions, 42 | }); 43 | 44 | (async function () { 45 | await generator.run(); 46 | })(); 47 | -------------------------------------------------------------------------------- /juejin/cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yang-first-pkg", 3 | "version": "1.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "yang-first-pkg", 9 | "version": "1.0.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "yang-first-pkg": "^1.0.1" 13 | }, 14 | "bin": { 15 | "yang": "bin/yang.js" 16 | } 17 | }, 18 | "node_modules/yang-first-pkg": { 19 | "version": "1.0.1", 20 | "resolved": "https://registry.npmjs.org/yang-first-pkg/-/yang-first-pkg-1.0.1.tgz", 21 | "integrity": "sha512-4+0AzINc6fn+4ft+eD4cqLXsMoIF2nSqm4xvQb8WZax/G6SDtnAAflzR7pInC1OJ5FsLVWKkODiYg64wQn8qwg==", 22 | "bin": { 23 | "yang": "bin/yang.js" 24 | } 25 | } 26 | }, 27 | "dependencies": { 28 | "yang-first-pkg": { 29 | "version": "1.0.1", 30 | "resolved": "https://registry.npmjs.org/yang-first-pkg/-/yang-first-pkg-1.0.1.tgz", 31 | "integrity": "sha512-4+0AzINc6fn+4ft+eD4cqLXsMoIF2nSqm4xvQb8WZax/G6SDtnAAflzR7pInC1OJ5FsLVWKkODiYg64wQn8qwg==" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import esbuild from '@umijs/bundler-utils/compiled/esbuild'; 2 | import { register } from '@umijs/utils' 3 | // plugin是为了获得PluginAPi 然后在指定的生命周期添加对应的功能 4 | 5 | export class Plugin{ 6 | private cwd; 7 | type; 8 | path; 9 | constructor(opts) { 10 | this.type = opts.type; 11 | this.path = opts.path; 12 | this.cwd = opts.cwd; 13 | } 14 | 15 | static getPluginsAndPresets(opts: { 16 | cwd: string; 17 | pkg: any; 18 | userConfig: any; 19 | plugins?: string[]; 20 | presets?: string[]; 21 | prefix: string; 22 | }) { 23 | 24 | } 25 | 26 | apply() { 27 | register.register({ 28 | implementor: esbuild, 29 | exts: ['.ts', '.mjs'], 30 | }); 31 | register.clearFiles(); 32 | let ret; 33 | try { 34 | ret = require(this.path); 35 | } catch (e: any) { 36 | throw new Error( 37 | `Register ${this.type} ${this.path} failed, since ${e.message}`, 38 | ); 39 | } finally { 40 | register.restore(); 41 | } 42 | // use the default member for es modules 43 | return ret.__esModule ? ret.default : ret; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /packages/preset-umi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mini-umi/preset-umi", 3 | "version": "1.3.4", 4 | "description": "", 5 | "module": "nodenext", 6 | "main": "dist/index.js", 7 | "bin": { 8 | "mini-umi": "./bin/mini-umi.js" 9 | }, 10 | "scripts": { 11 | "dev": "father dev", 12 | "build": "father build", 13 | "publish:all": "pnpm publish --no-git-checks", 14 | "build:deps": "father prebundle", 15 | "prepublishOnly": "father doctor && npm run build" 16 | }, 17 | "dependencies": { 18 | "@mini-umi/core": "workspace:*", 19 | "@rollup/plugin-node-resolve": "^15.0.1", 20 | "@umijs/utils": "^4.0.3", 21 | "@vitejs/plugin-vue": "^3.2.0", 22 | "@vitejs/plugin-vue-jsx": "^2.1.1", 23 | "compression": "^1.7.4", 24 | "esbuild": "^0.15.16", 25 | "express": "^4.18.2", 26 | "fs-extra": "^11.1.0", 27 | "rollup-plugin-css-only": "^4.3.0", 28 | "rollup-plugin-vue": "^6.0.0", 29 | "serve-static": "^1.15.0", 30 | "vite": "^3.2.4", 31 | "vite-plugin-ssr": "^0.4.54", 32 | "vue": "^3.2.45" 33 | }, 34 | "files": [ 35 | "dist", 36 | "template", 37 | "ssrtemplate", 38 | "src/vite.config.ts" 39 | ], 40 | "repository": "https://github.com/BoyYangzai/mini-umi", 41 | "keywords": [ 42 | "umi", 43 | "umijs", 44 | "mini-umi" 45 | ], 46 | "author": "洋", 47 | "license": "ISC" 48 | } 49 | -------------------------------------------------------------------------------- /packages/preset-umi/ssrtemplate/prerender.js.tpl: -------------------------------------------------------------------------------- 1 | // Pre-render the app into static HTML. 2 | // run `npm run generate` and then `dist/static` can be served as a static site. 3 | 4 | import fs from 'node:fs' 5 | import path from 'node:path' 6 | import url from 'node:url' 7 | 8 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) 9 | 10 | const toAbsolute = (p) => path.resolve(__dirname, p) 11 | 12 | const manifest = JSON.parse( 13 | fs.readFileSync(toAbsolute('./static/ssr-manifest.json'), 'utf-8') 14 | ) 15 | const template = fs.readFileSync(toAbsolute('./static/index.html'), 'utf-8') 16 | const { render } = await import('./server/entry-server.js') 17 | 18 | // determine routes to pre-render from src/pages 19 | const routesToPrerender = {{{routes}}} 20 | 21 | ; (async () => { 22 | // pre-render each route... 23 | for (const url of routesToPrerender) { 24 | const [appHtml, preloadLinks] = await render(url, manifest) 25 | 26 | const html = template 27 | .replace(``, preloadLinks) 28 | .replace(``, appHtml) 29 | 30 | const filePath = `./static${url === '/' ? '/index' : url}.html` 31 | fs.writeFileSync(toAbsolute(filePath), html) 32 | console.log('pre-rendered:', filePath) 33 | } 34 | 35 | // done, delete ssr manifest 36 | fs.unlinkSync(toAbsolute('./static/ssr-manifest.json')) 37 | })() 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{github.workflow}}-${{github.event_name}}-${{github.ref}} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | ci: 11 | runs-on: macos-latest 12 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 13 | strategy: 14 | matrix: 15 | node-version: [16] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: pnpm/action-setup@v2 20 | with: 21 | version: 7.14.0 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'pnpm' 28 | 29 | - name: Install dependencies 30 | run: pnpm install 31 | 32 | - name: Monorepo Build Production 33 | run: pnpm run build 34 | 35 | - name: Workflow failed alert 36 | if: ${{ failure() }} 37 | uses: actions-cool/maintain-one-comment@main 38 | with: 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | body: | 41 | 你好, @${{ github.event.pull_request.user.login }} CI 执行失败, 请点击 [Details] 按钮查看, 并根据日志修复 42 | Hello, @${{ github.event.pull_request.user.login }} CI run failed, please click the [Details] button for detailed log information and fix it. 43 | 44 | emojis: 'eyes' 45 | body-include: '' 46 | -------------------------------------------------------------------------------- /examples/vue3/pages/page-a.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 我是 /page-a 路由 😊 7 | 8 | 9 | 10 | 11 | 12 | mini-umi 13 | 14 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 15 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 16 | 17 | 18 | To be continued... 19 | mini-umi + preset-React + preset-MFSU 20 | mini-umi + preset-qinkun 21 | father dumi 22 | 23 | 24 | 25 | 26 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/pages/page-a.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 我是 /page-a 路由 😊 7 | 8 | 9 | 10 | 11 | 12 | mini-umi 13 | 14 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 15 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 16 | 17 | 18 | To be continued... 19 | mini-umi + preset-React + preset-MFSU 20 | mini-umi + preset-qinkun 21 | father dumi 22 | 23 | 24 | 25 | 26 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /examples/vue3/docs/test/testpage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 我是 /test/testpage 路由 qwq 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | mini-umi 14 | 15 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 16 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 17 | 18 | 19 | To be continued... 20 | mini-umi + preset-React + preset-MFSU 21 | mini-umi + preset-qinkun 22 | father dumi 23 | 24 | 25 | 26 | 27 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /examples/vue3/docs/page-a.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 我是 /page-a 路由 😊 7 | 8 | 9 | 10 | 11 | 12 | mini-umi 13 | 14 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 15 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 16 | 17 | 18 | To be continued... 19 | mini-umi + preset-React + preset-MFSU 20 | mini-umi + preset-qinkun 21 | father dumi 22 | 23 | 24 | 25 | 26 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /examples/vue3/pages/test/testpage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 我是 /test/testpage 路由 qwq 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | mini-umi 14 | 15 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 16 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 17 | 18 | 19 | To be continued... 20 | mini-umi + preset-React + preset-MFSU 21 | mini-umi + preset-qinkun 22 | father dumi 23 | 24 | 25 | 26 | 27 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/docs/page-a.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 我是 /page-a 路由 😊 7 | 8 | 9 | 10 | 11 | 12 | mini-umi 13 | 14 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 15 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 16 | 17 | 18 | To be continued... 19 | mini-umi + preset-React + preset-MFSU 20 | mini-umi + preset-qinkun 21 | father dumi 22 | 23 | 24 | 25 | 26 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/docs/test/testpage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 我是 /test/testpage 路由 qwq 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | mini-umi 14 | 15 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 16 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 17 | 18 | 19 | To be continued... 20 | mini-umi + preset-React + preset-MFSU 21 | mini-umi + preset-qinkun 22 | father dumi 23 | 24 | 25 | 26 | 27 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/pages/test/testpage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 我是 /test/testpage 路由 qwq 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | mini-umi 14 | 15 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 16 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 17 | 18 | 19 | To be continued... 20 | mini-umi + preset-React + preset-MFSU 21 | mini-umi + preset-qinkun 22 | father dumi 23 | 24 | 25 | 26 | 27 | 32 | 33 | 69 | -------------------------------------------------------------------------------- /juejin/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | "module": "ESNext", /* Specify what module code is generated. */ 5 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 6 | "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 7 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 8 | "sourceMap": true, 9 | "outDir": "dist", /* Create source map files for emitted JavaScript files. */ 10 | "declarationDir": "dist", /* Specify the output directory for generated declaration files. */ 11 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 12 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 13 | "strict": true, /* Enable all strict type-checking options. */ 14 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 15 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /手写可插拔前端框架小册/0.小册简介-手写可插拔前端框架Umi.md: -------------------------------------------------------------------------------- 1 | ### 你将获得 2 | 3 | 1. 类库开发,从 0 基础到掌握开源类库开发的所有知识; 4 | 1. 新人友好,手摸手教你从 0 开始造轮子; 5 | 1. 深入浅出,理解微内核架构、拓展无限可能; 6 | 1. 手写内核,一步一步实现 umi 的微内核架构; 7 | 1. 定制框架,教你搭建企业级可插拔定制框架。 8 | 9 | ### 作者介绍 10 | 11 | 前百度、现蚂蚁体验技术部 AntV 开源团队实习生,牛客千粉博主,喜欢探索前端工程化解决方案,热爱开源,喜欢分享技术,同时 **AntV** 的**Member**,**Umi、Dumi** 的**Contributer** 12 | 13 | ### 适宜人群: 14 | 15 | 在校大学生 ,初、中级前端工程师 16 | 17 | - 对前端开源社区中类库开发、框架开发、基础架构相关感兴趣的前端工程师 18 | - 对前端工程化感兴趣,想要学习如何参与开源,但没有系统学习认识的在校大学生和初级前端工程师 19 | - 一起阅读源码,和作者一起学习优秀前端工程师们的工程实践经验 20 | - 对于 Umi 微内核架构感兴趣,对如何基于 Umi 内核拓展能力开发例如蚂蚁金服的中后台框架-Bigfish,npm 包研发工具 father,以及刚刚推出的 dumi2 感兴趣的前端工程师 21 | 22 | ### 小册介绍: 23 | 24 | 如果你现在去使用 Umi,你会发现它是一个类似于 Nextjs、Remix 一个样的前端框架,它有很多功能:约定式路由、SSR、MOCK 数据、配置文件... 25 | 26 | 像蚂蚁金服内部的中后台前端框架 Bigfish 其实就是基于 Umi 框架封装的,类似的还有 Dumi、father 这两个框架 27 | 28 | 如果你对如何手写实现这样的前端框架感兴趣,这本小册你一定不能错过 29 | 30 |  31 | 32 | #### 什么是微内核架构? 33 | 34 | 如果你平时喜欢玩游戏,那你一定对 MOD(模组)这个词并不陌生 35 | 36 | 通过微内核暴露出来的 API,你可以自定义各式各样的模组去实现任何你想要的功能,比方说给你的游戏角色换个衣服,比方说给你的前端框架加个 SSR 的模式,只不过在前端领域我们把它叫做 Plugin-插件 37 | 38 |  39 | 40 | 在本课程中,我分了三个大部分来系统讲述: 41 | 42 |  43 | 44 | **1.前端工程化-类库开发** 45 | 46 | 系统认识如何开发好用的 npm 类库与框架,每一小节都配备了充足的实战项目,保证新手同学也能看得懂,学得下去,为后续手写内核和框架学习必要工程化知识 47 | 48 | **2.手写微内核架构-实现 mini-core** 49 | 50 | 在这一大章,你将认识微内核架构的原理与实现,并亲手实现微内核架构的各个模块,如 Service、Plugin、PluginAPI、可扩展插件系统、应用元数据等 51 | 52 | **3.手写企业级中后台前端框架** 53 | 54 | 这一大章你已经实现了自己的微内核架构,我们将在微内核架构的基础上,手写实现一个企业级可用的中后台前端框架-mini-umi 55 | -------------------------------------------------------------------------------- /packages/core/src/pluginAPI.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "./command"; 2 | import { Hook } from "./hook"; 3 | import { type Core } from './core' 4 | 5 | 6 | export class PluginAPi{ 7 | service; 8 | plugin; 9 | onStart: Function =()=>{}; 10 | constructor(opts: { service; plugin}) { 11 | this.service = opts.service; 12 | this.plugin = opts.plugin 13 | } 14 | 15 | register(opts: { key: string, fn: any }) { 16 | (this.service.hooks[opts.key] || (this.service.hooks[opts.key] = [])).push(new Hook({ ...opts, plugin: this.plugin })) 17 | } 18 | 19 | 20 | registerCommand(opts:{name:string,fn:Function}) { 21 | const { name } = opts 22 | this.service.commands[name] = new Command({...opts,plugin:this.plugin}) 23 | } 24 | 25 | registerMethod(opts: { name: string, fn?: Function }) { 26 | this.service.pluginMethods[opts.name] = { 27 | plugin: this.plugin, 28 | fn: 29 | opts.fn || 30 | function (fn: Function) { 31 | this.register({ 32 | key: opts.name, 33 | fn 34 | }); 35 | }, 36 | }; 37 | } 38 | 39 | static proxyPluginAPI(opts: { 40 | pluginAPI: PluginAPi; 41 | service: Core; 42 | serviceProps: string[]; 43 | staticProps: Record; 44 | }) { 45 | return new Proxy(opts.pluginAPI, { 46 | get: (target, prop: string) => { 47 | if (opts.service.pluginMethods[prop]) { 48 | return opts.service.pluginMethods[prop].fn; 49 | } 50 | if (opts.serviceProps.includes(prop)) { 51 | // @ts-ignore 52 | const serviceProp = opts.service[prop]; 53 | return typeof serviceProp === 'function' 54 | ? serviceProp.bind(opts.service) 55 | : serviceProp; 56 | } 57 | if (prop in opts.staticProps) { 58 | return opts.staticProps[prop]; 59 | } 60 | // @ts-ignore 61 | return target[prop]; 62 | }, 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/vue3/docs/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 约定式路由+可插拔插件系统+mumirc.ts/js配置文件+Vite毫秒级开发体验 7 | 8 | 9 | 10 | 支持自定义约定式路由目录~ 可在配置文件中更改 11 | 12 | 13 | 14 | 15 | 16 | mini-umi 17 | 18 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 19 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 20 | 21 | 22 | To be continued... 23 | mini-umi + preset-React + preset-MFSU 24 | mini-umi + preset-qinkun 25 | father dumi 26 | 27 | 28 | 29 | 30 | 41 | 42 | 77 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/docs/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 约定式路由+可插拔插件系统+mumirc.ts/js配置文件+Vite毫秒级开发体验 7 | 8 | 9 | 10 | 支持自定义约定式路由目录~ 可在配置文件中更改 11 | 12 | 13 | 14 | 15 | 16 | mini-umi 17 | 18 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 19 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 20 | 21 | 22 | To be continued... 23 | mini-umi + preset-React + preset-MFSU 24 | mini-umi + preset-qinkun 25 | father dumi 26 | 27 | 28 | 29 | 30 | 41 | 42 | 77 | -------------------------------------------------------------------------------- /examples/vue3/localUserRoute.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 约定式路由+可插拔插件系统+mumirc.ts/js配置文件+Vite毫秒级开发体验 6 | 7 | 8 | 9 | 我是用户自定义路由 10 | 11 | 12 | 13 | 14 | 15 | mini-umi 16 | 17 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 18 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 19 | 20 | 21 | To be continued... 22 | mini-umi + preset-React + preset-MFSU 23 | mini-umi + preset-qinkun 24 | father dumi 25 | 26 | 27 | 28 | 29 | 40 | 41 | 82 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/localUserRoute.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 约定式路由+可插拔插件系统+mumirc.ts/js配置文件+Vite毫秒级开发体验 6 | 7 | 8 | 9 | 我是用户自定义路由 10 | 11 | 12 | 13 | 14 | 15 | mini-umi 16 | 17 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 18 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 19 | 20 | 21 | To be continued... 22 | mini-umi + preset-React + preset-MFSU 23 | mini-umi + preset-qinkun 24 | father dumi 25 | 26 | 27 | 28 | 29 | 40 | 41 | 82 | -------------------------------------------------------------------------------- /examples/vue3/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 约定式路由+可插拔插件系统+mumirc.ts/js配置文件+Vite毫秒级开发体验 7 | 8 | 9 | 已完成:客户端渲染、服务端渲染 通过指令切换 TODO: SSG(暂时有点小bug) 10 | 11 | 12 | 点我跳转 page-a 路由 13 | 14 | 点我跳转 test/test-page 路由 15 | 16 | 17 | 18 | 19 | 20 | mini-umi 21 | 22 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 23 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 24 | 25 | 26 | To be continued... 27 | mini-umi + preset-React + preset-MFSU 28 | mini-umi + preset-qinkun 29 | father dumi 30 | 31 | 32 | 33 | 34 | 45 | 46 | 81 | -------------------------------------------------------------------------------- /packages/create-m-umi/templates/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 约定式路由+可插拔插件系统+mumirc.ts/js配置文件+Vite毫秒级开发体验 7 | 8 | 9 | 10 | 已完成:客户端渲染、服务端渲染 通过指令切换 TODO: SSG(暂时有点小bug) 11 | 12 | 13 | 点我跳转 page-a 路由 14 | 15 | 点我跳转 test/test-page 路由 16 | 17 | 18 | 19 | 20 | 21 | mini-umi 22 | 23 | 基于Umi微内核架构搭建的 可插拔 渐进式框架 24 | 本框架采用 mini-umi + preset-Vue3.2 + Vite 25 | 26 | 27 | To be continued... 28 | mini-umi + preset-React + preset-MFSU 29 | mini-umi + preset-qinkun 30 | father dumi 31 | 32 | 33 | 34 | 35 | 46 | 47 | 82 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/getRoutes.ts: -------------------------------------------------------------------------------- 1 | import { fsExtra,winPath} from '@umijs/utils' 2 | import { join,isAbsolute ,basename} from 'path' 3 | type routes = { 4 | path: string 5 | name: string 6 | component?: Function 7 | children?:routes 8 | }[] 9 | export function getRoutes(opts: { dirPath: string } = { dirPath: './pages' }) { 10 | 11 | const routes: routes = [] 12 | const cwd =process.cwd() 13 | 14 | const routesDir = isAbsolute(opts.dirPath) ? opts.dirPath : winPath(join(cwd, opts.dirPath)) 15 | const dirname = basename(routesDir) 16 | 17 | 18 | function dps(pPath: string = '', dir = routesDir) { 19 | const alllFiles = fsExtra.readdirSync(dir) 20 | const dirFiles = alllFiles.filter(item => !item.includes('.')) 21 | const componentFiles = alllFiles.filter(item => item.endsWith('.vue') || item.endsWith('.tsx')) 22 | 23 | const tRoutesArray=[] 24 | componentFiles.forEach(item => { 25 | const name = getName(item) 26 | let path = `${pPath}/${name}` 27 | if (path === '/index') { 28 | tRoutesArray.push({ 29 | path:'/', 30 | name: name, 31 | component: eval(`() => import('../${dirname}${path}.vue')`), 32 | }) 33 | } else { 34 | if (pPath !== '') { 35 | path = `/${path}` 36 | } 37 | tRoutesArray.push({ 38 | path, 39 | name: name, 40 | component: eval(`() => import('../${dirname}${path}.vue')`), 41 | }) 42 | } 43 | 44 | }) 45 | dirFiles.forEach(item => { 46 | const name: string = getName(item) 47 | if (name.startsWith(':')) { 48 | let path = `${pPath}/${name}` 49 | if (pPath !== '') { 50 | path = `/${path}` 51 | } 52 | tRoutesArray.push({ 53 | path, 54 | name: name, 55 | component: eval(`() => import('../${dirname}${path}/index.vue')`) 56 | }) 57 | } else { 58 | const path = `${dir}/${name}` 59 | tRoutesArray.push({ 60 | path: '', 61 | name: '', 62 | children: dps(name, path) 63 | }) 64 | } 65 | 66 | }) 67 | return tRoutesArray 68 | } 69 | routes.push(...dps()) 70 | 71 | function getName(fileName) { 72 | return fileName.replace('.vue','').replace('.tsx','') 73 | } 74 | 75 | return routes 76 | } 77 | -------------------------------------------------------------------------------- /手写可插拔前端框架小册/技术收敛-什么是微内核架构.md: -------------------------------------------------------------------------------- 1 | ## 总览 Umi 微内核架构: 2 | 3 | ```mermaid 4 | graph TD 5 | 6 | PluginAPI -->很多插件A 7 | PluginAPI -->很多插件B 8 | PluginAPI -->很多插件C 9 | PluginAPI -->很多插件D 10 | PluginAPI -->很多插件E 11 | PluginAPI -->很多插件F 12 | PluginAPI -->很多插件X 13 | 很多插件A -->preset-father 14 | 很多插件B -->preset-dumi 15 | 很多插件C -->preset-xx 16 | 很多插件D -->preset-umi 17 | 很多插件E -->preset-qiankun-可选 18 | 很多插件F -->preset-mfsu-可选 19 | 很多插件X -->preset-xxx-可选 20 | 内核Core --> PluginAPI 21 | 内核Core --> father 22 | preset-father --> father 23 | preset-dumi --> Dumi 24 | preset-xx --> xx框架 25 | 内核Core --> xx框架 26 | 内核Core --> Umi 27 | preset-umi --> Umi 28 | rendered-react/rendered-vue --> preset-umi 29 | 内核Core --> Dumi 30 | preset-qiankun-可选 --> Umi 31 | preset-mfsu-可选 --> Umi 32 | preset-xxx-可选 --> Umi 33 | ``` 34 | 35 | Core: 36 | 37 | - commands { cmd1:fn , cmd2:fn ...} 38 | - hooks { key1:fn[] , key2:fn[] ...} 39 | - 应用元数据 40 | - presets 41 | - plugins 42 | - ... 43 | 44 | PluginAPI: 45 | 46 | ```ts 47 | export default (api: IAPI) => { 48 | api.registerCommand({ 49 | key: '', 50 | fn(){} 51 | }) 52 | } 53 | 54 | api.applyPlugins({ 55 | key: '', 56 | initValue?: '' 57 | }) 58 | } 59 | ``` 60 | 61 | 初始化阶段:按照一定的顺序依次获取所有 Presets 和 Plugin 62 | 63 | 64 | 内核 Core 提供 PluginAPU--> 用户编写插件-->插件组合+内核 = 框架 65 | 66 | ```mermaid 67 | graph TD 68 | 内核Core --> PluginAPI-提供能力 69 | PluginAPI-提供能力 --> 插件A-功能A 70 | PluginAPI-提供能力 --> 插件B-功能B 71 | PluginAPI-提供能力 --> 插件C-功能C 72 | PluginAPI-提供能力 --> 插件D-功能D 73 | PluginAPI-提供能力 --> 插件X-功能A 74 | 插件A-功能A --> preset-XX 75 | 插件B-功能B --> preset-XX 76 | 插件C-功能C --> preset-XX 77 | 插件D-功能D --> preset-XX 78 | 插件X-功能X --> preset-XX 79 | ``` 80 | 81 | 插件 + 预设 = Umi 框架 82 | 83 | 84 | ```mermaid 85 | graph TD 86 | 内核Core --> Umi框架 87 | preset-umi --> Umi框架 88 | rendered-react/rendered-vue --> preset-umi 89 | preset-qiankun-可选 --> Umi框架 90 | preset-mfsu-可选 --> Umi框架 91 | preset-vue-ssr-可选 --> Umi框架 92 | preset-xxx-可选 --> Umi框架 93 | ``` 94 | 95 | 96 | 不同插件 + 不同预设 = 不同框架 97 | 98 | ```mermaid 99 | graph TD 100 | 核心:内核Core --> father框架 101 | preset-father --> father框架 102 | preset-dumi --> Dumi框架 103 | 核心:内核Core --> Dumi框架 104 | 核心:内核Core --> Umi框架-需要加上很多preset 105 | preset-XX --> XX框架 106 | 核心:内核Core --> XX框架 107 | 108 | ``` 109 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import { type ICoreApi } from '@mini-umi/core' 2 | import { 3 | chalk, 4 | winPath, 5 | } from '@umijs/utils'; 6 | import { IpresetUmi } from '../types'; 7 | import { build as viteBuild } from 'vite' 8 | import { join, resolve } from 'path' 9 | import vue from 'rollup-plugin-vue' 10 | import css from 'rollup-plugin-css-only' 11 | import nodeResolve from '@rollup/plugin-node-resolve' 12 | import { getRoutesString } from './utils'; 13 | import { getRoutes } from './getRoutes'; 14 | 15 | export default (api: ICoreApi & IpresetUmi) => { 16 | const cwd = process.cwd() 17 | api.registerCommand({ 18 | name: 'build', 19 | async fn() { 20 | // directCopyFiles 21 | const directCopyFiles = ['app.vue', 'main.ts', 'index.html'] 22 | directCopyFiles.forEach(fileName => { 23 | api.writeTmpFile({ 24 | target: winPath(join(cwd, `./.mini-umi/${fileName}`)), 25 | path: `./${fileName}.tpl`, 26 | data: { 27 | } 28 | }) 29 | }); 30 | 31 | // routes.ts 32 | const routes = getRoutes() 33 | const routesString = getRoutesString(routes) 34 | await api.writeTmpFile({ 35 | target: winPath(join(cwd, `./.mini-umi/routes.ts`)), 36 | path: `./routes.ts.tpl`, 37 | data: { 38 | routes: routesString 39 | } 40 | }); 41 | 42 | const userViteConfig = await api.applyPlugins({ 43 | key: 'modifyViteConfig', 44 | initialValue: api.config!.viteConfig 45 | }) 46 | 47 | // build 48 | await viteBuild({ 49 | ...userViteConfig, 50 | root: resolve(cwd, './.mini-umi'), 51 | base: './', 52 | build: { 53 | rollupOptions: { 54 | plugins: [ 55 | css(), 56 | vue({ css: false }), 57 | nodeResolve() 58 | ] 59 | } 60 | } 61 | }) 62 | console.log(); 63 | console.log(); 64 | console.log(); 65 | 66 | console.log( 67 | chalk.greenBright('----------构建产物成功-----------') 68 | ); 69 | console.log(); 70 | console.log(); 71 | console.log( 72 | `${chalk.yellowBright('请使用')} ${chalk.blueBright('npm run preview')} ${ chalk.yellowBright('预览') }` 73 | ); 74 | console.log(); 75 | console.log(); 76 | } 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /packages/preset-umi/ssrtemplate/entry-server.js: -------------------------------------------------------------------------------- 1 | import { basename } from 'node:path' 2 | import { renderToString } from 'vue/server-renderer' 3 | import { createApp } from './main' 4 | 5 | export async function render(url, manifest) { 6 | const { app, router } = createApp() 7 | 8 | // set the router to the desired URL before rendering 9 | await router.push(url) 10 | await router.isReady() 11 | 12 | // passing SSR context object which will be available via useSSRContext() 13 | // @vitejs/plugin-vue injects code into a component's setup() that registers 14 | // itself on ctx.modules. After the render, ctx.modules would contain all the 15 | // components that have been instantiated during this render call. 16 | const ctx = {} 17 | const html = await renderToString(app, ctx) 18 | 19 | // the SSR manifest generated by Vite contains module -> chunk/asset mapping 20 | // which we can then use to determine what files need to be preloaded for this 21 | // request. 22 | const preloadLinks = renderPreloadLinks(ctx.modules, manifest) 23 | return [html, preloadLinks] 24 | } 25 | 26 | function renderPreloadLinks(modules, manifest) { 27 | let links = '' 28 | const seen = new Set() 29 | modules.forEach((id) => { 30 | const files = manifest[id] 31 | if (files) { 32 | files.forEach((file) => { 33 | if (!seen.has(file)) { 34 | seen.add(file) 35 | const filename = basename(file) 36 | if (manifest[filename]) { 37 | for (const depFile of manifest[filename]) { 38 | links += renderPreloadLink(depFile) 39 | seen.add(depFile) 40 | } 41 | } 42 | links += renderPreloadLink(file) 43 | } 44 | }) 45 | } 46 | }) 47 | return links 48 | } 49 | 50 | function renderPreloadLink(file) { 51 | if (file.endsWith('.js')) { 52 | return `` 53 | } else if (file.endsWith('.css')) { 54 | return `` 55 | } else if (file.endsWith('.woff')) { 56 | return ` ` 57 | } else if (file.endsWith('.woff2')) { 58 | return ` ` 59 | } else if (file.endsWith('.gif')) { 60 | return ` ` 61 | } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) { 62 | return ` ` 63 | } else if (file.endsWith('.png')) { 64 | return ` ` 65 | } else { 66 | // TODO 67 | return '' 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### 你将获得 2 | 3 | 1. 类库开发,从0基础到掌握开源类库开发的所有知识; 4 | 1. 新人友好,手摸手教你从0开始造轮子; 5 | 1. 深入浅出,理解微内核架构、拓展无限可能; 6 | 1. 手写内核,一步一步实现umi的微内核架构; 7 | 1. 定制框架,教你搭建企业级可插拔定制框架。 8 | 9 | ### 作者介绍 10 | 前百度、现蚂蚁体验技术部 AntV 开源团队实习生,牛客千粉博主,喜欢探索前端工程化解决方案,热爱开源,喜欢分享技术,同时也是 **AntV** 和 **Umi** 团队的 **Member**, 11 | ### 适宜人群: 12 | 13 | 在校大学生 ,初、中级前端工程师 14 | 15 | - 对前端开源社区中类库开发、框架开发、基础架构相关感兴趣的前端工程师 16 | - 对前端工程化感兴趣,想要学习如何参与开源,但没有系统学习认识的在校大学生和初级前端工程师 17 | - 一起阅读源码,和作者一起学习优秀前端工程师们的工程实践经验 18 | - 对于Umi微内核架构感兴趣,对如何基于Umi内核拓展能力开发例如蚂蚁金服的中后台框架-Bigfish,npm包研发工具father,以及刚刚推出的dumi2 感兴趣的前端工程师 19 | 20 | ### 小册介绍: 21 | 22 | 如果你现在去使用 Umi,你会发现它是一个类似于Nextjs、Remix一个样的前端框架,它有很多功能:约定式路由、SSR、MOCK数据、配置文件... 23 | 24 | 像蚂蚁金服内部的中后台前端框架Bigfish其实就是基于Umi框架封装的,类似的还有Dumi、father这两个框架 25 | 26 | 如果你对如何手写实现这样的前端框架感兴趣,这本小册你一定不能错过 27 |  28 | 上图来自 Umi 官网 29 | #### 什么是微内核架构? 30 | 31 | 如果你平时喜欢玩游戏,那你一定对MOD(模组)这个词并不陌生 32 | 33 | 通过微内核暴露出来的API,你可以自定义各式各样的模组去实现任何你想要的功能,比方说给你的游戏角色换个衣服,比方说给你的前端框架加个SSR的模式,只不过在前端领域我们把它叫做Plugin-插件 34 | 35 |  36 | 37 | 在本课程中,我分了三个大部分来系统讲述: 38 | 39 |  40 | 41 | **1.前端工程化-类库开发** 42 | 43 | 系统认识如何开发好用的npm类库与框架,每一小节都配备了充足的实战项目,保证新手同学也能看得懂,学得下去,为后续手写内核和框架学习必要工程化知识 44 | 45 | **2.手写微内核架构-实现mini-core** 46 | 47 | 在这一大章,你将认识微内核架构的原理与实现,并亲手实现微内核架构的各个模块,如Service、Plugin、PluginAPI、可扩展插件系统、应用元数据等 48 | 49 | **3.手写企业级中后台前端框架** 50 | 51 | 这一大章你已经实现了自己的微内核架构,我们将在微内核架构的基础上,手写实现一个企业级可用的中后台前端框架-mini-umi 52 | 53 | ## 体验 mini-umi + Vue3.2 + Vite预设 54 | > ✅ Monorepo 最佳实践 + Turbo 远端构建缓存 55 | ``` 56 | npx create-mumi 项目名 57 | npm install 58 | npm run dev 59 | npm run ssr // 服务端渲染模式 60 | ``` 61 | ## 实现 Service Core 架构的最简模型 62 | 😚已完成: 63 | 64 | ✅ 实现内置 presets plugin 功能 65 | 66 | ✅ 实现 mini-umi 的 command 系统 67 | 68 | ✅ 实现编译时 Hook 69 | 70 | ✅ feat: 读取用户 Local Plugin 71 | 72 | ✅ 完善插件API 73 | 74 | ✅ 实现 create-mumi 脚手架 75 | 76 | ✅ 实现 userConfig 以及 modify 全流程 77 | 78 | ✅ 实现 Preset-Vue3.2 + Vite + dev build preview 79 | 80 | ✅ 实现约定式路由 支持动态路由 Vite 支持 Vue3.2 Hmr 81 | 82 | ✅ 支持对于 dev 约定式路由 的热更新 83 | 84 | ✅ 支持以 LocalPlugin 的形式 ModifyBundleConfig 85 | 86 | ✅ 支持用户配置文件自定义路由 和 更改约定式路由所在目录 87 | 88 | ✅ 可通过 npm run ssr 开启服务端渲染模式 89 | 90 | ✅ CSR SSR 支持用户自定义Layout 91 | 92 | ... 93 | 94 | 🤔More: 95 | - [ ] 实现一套 preset-react 96 | - [ ] 实现一套 preset-qiankun 97 | - [ ] 实现 Father 98 | - [ ] 实现 Dumi 99 | - [x] 实现一个新的应用型前端框架 - [Doctor](https://github.com/FE-Struggler/doctor) 100 | 101 | -------------------------------------------------------------------------------- /手写可插拔前端框架小册/1-1.前端漫谈-npm包与框架.md: -------------------------------------------------------------------------------- 1 | ### 该小节主要知识点: 2 | 3 | - 什么是 `npm包` 4 | - ⭐️ 什么是 `框架` 5 | - `为什么`要学习和编写 npm 包 6 | - 实现一个自己的 npm 包 `需要什么准备` 7 | 8 | ## 什么是 npm? 9 | 10 | npm 即 Node Package(包) Manager(管理器) 11 | 是一个 `Nodejs` 的 **包管理平台** 12 | 13 | ## 什么是 npm 包? 14 | 15 | **npm 包说白了其实就是一个具有`一定功能`的 `Nodejs 模块`** 16 | 17 | 我们可以通过将这个模块`发布`到 npm 仓库上达到`便捷`的`模块共享` 18 | 19 | ## 什么是 `'模块'` ? 20 | 21 | tips: 在`第5小节`将为大家介绍模块化的发展史 22 | 23 | 在前端从刀耕火种的年代到如今的工程化兴起,**伴随着`工程化`相生的另一个词叫** -- **`模块化`** 24 | 25 | **CJS、ESM 都是模块化的代名词,相信大家早就耳熟能详了** 26 | 27 | 28 | 我们通过这种 **`模块化的形式`** 可以**将一些`具有特定功能`的代码`抽离`出去**达到 **`逻辑复用`** 的效果,这样往往能`节省`大量的时间和人力`资源` 29 | 30 | 31 | 32 | **近期大多数前端在进行开发时都用到的 比如:** 33 | 34 | **常见组件库:** 35 | 36 | - 蚂蚁的 `AntDesign`、阿里的 `ElementPlus`、`Element ui` ... 37 | 38 | **常见业务钩子库:** 39 | 40 | - `react-use`、`ahooks`、`vue-use` ... 41 | 42 | **常用工具库:** 43 | 44 | - `lodash`、`Axios`、`@umijs/utils` ... 45 | 46 | **常用打包工具:** 47 | 48 | - `Webpack`、`Rollup`、`Gulp` 、`Turbopack` ... 49 | 50 | **常用编译工具:** 51 | 52 | - `tsc`、`babel`、`esbuild`、`swc` ... 53 | 54 | **常见框架:** 55 | 56 | - `React`、`Vue`、`Svelte`、` Nuxtjs``、Nextjs `、`Umijs` ... 57 | 58 | 其实我们在使用这些我们**耳熟能详**的库的时候,**大多数情况**都是通过 **`npm包`** 的形式**引入**的(当然还有比如用 CDN 引入等),他们会作为一个`特定的功能模块`去完成我们的需求--`方便`! 59 | 60 | 如果你在业务中发现了某种 **`业务痛点`** 并将 **`解决思路`** 封装成一个`优质的npm包`,相信我,它的 **star** 一定不会少 61 | 62 | 因为 **`你遇到的业务痛点`、开发上遇到的`技术难点`往往也是`大多数开发者所苦恼`的** 63 | 64 | ## 什么是`'框架'`? 65 | 66 | 打个比方: 67 | 68 | **框架是`工厂`**,服务于社会大众 这个工厂里面有很多的流水线 69 | 70 | **npm 库是`流水线`** 每个流水线都有自己的 **`特定的功能/任务`** 71 | 72 | 但**工厂不仅仅是流水线的集合**,更重要的是**如何把流水线`组织`起来**,**`井然有序`** 的 **`保障交付的质量和效率`** 73 | 74 | --- 75 | 76 | 比方说拿大家最熟悉的[Vue2 仓库](https://github.com/vuejs/core)来看: 77 | 78 | 这里使用 **`monorepo`** 去管理整个代码仓库 方便本地开发与单独发包 79 | 80 | > tips: 后面的章节会详细的介绍 monorepo 以及它在 mini-umi 里的实践 81 | 82 | 只需要知道 **package 下的每个文件夹** 基本都是一个 **`独立`的 npm 包** 83 | 84 | --- 85 | 86 |  87 | 88 | 从上图 我们看名字就知道,这个包是编译 dom 的,这个包是编译 vue 的单文件 sfc 的,这个包是响应式核心、这个是一个编译 ssr 的,这个包是作用于运行时的... 89 | 90 | 上面这些包是 vue 吗 - 不是 91 | 92 | 最底下的那个 vue 包才是我们熟知的 框架-Vue 93 | 94 | 这些 npm 库确实实现了某一类功能,但是 **`框架通过一系列的组合制定了应用规则`**,**`保障了交付的品质`**,可以让我们 **用`低成本`使用`高品质`的工具** 95 | 96 | 当然,一**个好的框架还有很多东西要去考量**,比方说;**`框架的可扩展性`、`灵活性`,`开发中的规范`、`交付质量`** 等 97 | 98 | 这不仅仅是前端该思考的,作为程序开发的我们都要在这条路漫漫上下求索~ 99 | 100 | ## 为什么要学习和编写 npm 包? 101 | 102 | 说了这么多,npm 包和框架的重要性已经不言而喻了 103 | 104 | 有些刚毕业的前端做了一两年开发也没写过一个 npm 包,因为我们大多数人都是在做所谓的`切图`工作 105 | 106 | 什么?B 端 中后台?我会用 Antd ElementPlus 107 | 108 | 什么?C 端?我能还原页面 完成 css 动画 109 | 110 | 什么?小程序?跨端? 我能忍受拉胯的开发体验 调 api 写 css 111 | 112 | 可让一个实习生来干熟悉一两周 他也能干 所以我们没有竞争力 113 | 114 | 我们必须要有**更多更丰富的业务经验**去**提升我们的`价值`** 115 | 116 | **了解学习和编写 npm 库 学习工程化 扎实前端功底** 有助于我们**提升`核心竞争力`** 117 | 118 | ## 实现一个自己的 npm 包需要什么准备? 119 | 120 | 实现一个这样的 npm 库你需要: 121 | 122 | 1. 熟知基本的 npm 库开发流程 123 | 1. 保证 它可以在其他 Users 那里被正常使用 124 | 1. 思考用户的使用体验 例如:体积 导出的内容 等等 125 | 1. 了解如何发布一个 npm 包 126 | 127 | --- 128 | 129 | ## 小结: 130 | 131 | **本小节我们** 132 | 133 | **1.首先为大家介绍了什么是 npm 包以及举例说明前端开发中`常用`的一些 npm 类库 ,并概述它们大多都是将我们在开发过程中遇到的`业务痛点`封装起来的共性** 134 | 135 | **2.主要为大家介绍了什么是`框架`,框架与某个独立功能的 npm 类库 的区别,它制定了一系列应用规则去保证各流水线之间相互配合、顺利运转,提高开发者效率,保障交付质量** 136 | 137 | **3.通过前端现状为大家讲述学习 npm 包 也就是 类库开发 的`重要性`,目的是为了更好的扎实前端基础,提升自己的`核心竞争力`** 138 | 139 | **4.简单介绍实现一个 npm 包 需要准备什么** 140 | 141 | **下一小节将会为大家讲解如何一步一步实现一个第一个 npm 包`CLI`** 142 | 143 | ## 小节思考: 144 | 145 | **`什么是前端框架?` 它和单一功能的`前端类库`有什么`区别`** 146 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/dev.ts: -------------------------------------------------------------------------------- 1 | import { 2 | winPath, 3 | chalk, 4 | chokidar, 5 | deepmerge, 6 | fsExtra 7 | } from '@umijs/utils'; 8 | import { createServer} from 'vite' 9 | import { join } from 'path' 10 | import { getRoutesString } from './utils'; 11 | import { getRoutes } from './getRoutes'; 12 | import vue from '@vitejs/plugin-vue'; 13 | import { type ICoreApi } from '@mini-umi/core' 14 | import { IpresetUmi } from '../types'; 15 | 16 | export default (api: ICoreApi & IpresetUmi) => { 17 | 18 | const cwd = process.cwd() 19 | api.registerCommand({ 20 | name: 'dev', 21 | async fn() { 22 | 23 | // directCopyFiles 24 | const directCopyFiles = ['app.vue', 'main.ts', 'index.html'] 25 | directCopyFiles.forEach(fileName => { 26 | api.writeTmpFile({ 27 | target: winPath(join(cwd, `./.mini-umi/${fileName}`)), 28 | path: `./${fileName}.tpl`, 29 | data: { 30 | } 31 | }) 32 | }); 33 | 34 | // routes.ts 35 | async function resolveRoutes() { 36 | const routesDirPath = await api.applyPlugins({ 37 | key: 'modifyRoutesDir', 38 | initialValue: api.userConfig!.routesDir 39 | }) 40 | const userRoutes = api.userConfig.routes ? api.userConfig.routes:[] 41 | const routes = deepmerge(getRoutes({ 42 | dirPath: routesDirPath 43 | }), userRoutes) 44 | const routesString = getRoutesString(routes) 45 | await api.writeTmpFile({ 46 | target: winPath(join(cwd, `./.mini-umi/routes.ts`)), 47 | path: `./routes.ts.tpl`, 48 | data: { 49 | routes: routesString 50 | } 51 | }); 52 | } 53 | await resolveRoutes() 54 | 55 | 56 | // layout/index.vue 57 | function layout() { 58 | try { 59 | const layoutContent = fsExtra.readFileSync(winPath(join(cwd, './layout/index.vue')), 'utf-8') 60 | fsExtra.writeFileSync(winPath(join(cwd, './.mini-umi/App.vue')), layoutContent) 61 | } catch (err) { 62 | // no file 63 | } 64 | } 65 | layout() 66 | // start server 67 | const userViteConfig = await api.applyPlugins({ 68 | key: 'modifyViteConfig', 69 | initialValue: api.config!.viteConfig 70 | }) 71 | const defaultViteConfig = { 72 | plugins: [vue()] 73 | } 74 | // userViteConfig.plugins.push() 75 | const viteConfig = deepmerge(userViteConfig,defaultViteConfig) 76 | 77 | const server = await createServer({ 78 | ...viteConfig, 79 | root: join(process.cwd(), './.mini-umi'), 80 | server: { 81 | port: 8000, 82 | host: true 83 | } 84 | }) 85 | 86 | await server.listen() 87 | server.printUrls() 88 | 89 | console.log(); 90 | console.log(); 91 | console.log( 92 | chalk.greenBright('🎉🎉🎉恭喜你,mini-umi + Vue3.2 + Vite 启动成功!') 93 | ); 94 | console.log(); 95 | console.log(); 96 | console.log(); 97 | 98 | 99 | // 约定式路由重新生成 100 | chokidar.watch(join(cwd, './pages'), { 101 | ignoreInitial: true, 102 | }).on('all', async () => { 103 | await resolveRoutes() 104 | }) 105 | 106 | // layout 重新生成 107 | chokidar.watch(join(cwd, './layout'), { 108 | ignoreInitial: true, 109 | }).on('all', async () => { 110 | layout() 111 | await server.restart() 112 | }) 113 | 114 | } 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /packages/core/src/config/config.ts: -------------------------------------------------------------------------------- 1 | import esbuild from '@umijs/bundler-utils/compiled/esbuild'; 2 | import { lodash, register, semver } from '@umijs/utils'; 3 | import { existsSync } from 'fs'; 4 | import { join } from 'path' 5 | import { 6 | DEFAULT_CONFIG_FILES, 7 | LOCAL_EXT, 8 | SHORT_ENV, 9 | } from '../constants'; 10 | import { addExt, getAbsFiles } from './utils'; 11 | 12 | export enum Env { 13 | development = 'development', 14 | production = 'production', 15 | test = 'test', 16 | } 17 | 18 | interface IOpts { 19 | cwd: string; 20 | env: Env; 21 | specifiedEnv?: string; 22 | defaultConfigFiles?: string[]; 23 | } 24 | 25 | export class Config { 26 | public opts: IOpts; 27 | public mainConfigFile: string | null; 28 | public prevConfig: any; 29 | public files: string[] = []; 30 | 31 | constructor(opts: IOpts) { 32 | this.opts = opts; 33 | this.mainConfigFile = Config.getMainConfigFile(this.opts); 34 | this.prevConfig = null; 35 | } 36 | 37 | getUserConfig() { 38 | const configFiles = Config.getConfigFiles({ 39 | mainConfigFile: this.mainConfigFile, 40 | env: this.opts.env, 41 | specifiedEnv: this.opts.specifiedEnv, 42 | }); 43 | return Config.getUserConfig({ 44 | configFiles: getAbsFiles({ 45 | files: configFiles, 46 | cwd: this.opts.cwd, 47 | }), 48 | }); 49 | } 50 | 51 | static getMainConfigFile(opts: { 52 | cwd: string; 53 | defaultConfigFiles?: string[]; 54 | }) { 55 | let mainConfigFile = null; 56 | for (const configFile of opts.defaultConfigFiles || DEFAULT_CONFIG_FILES) { 57 | const absConfigFile = join(opts.cwd, configFile); 58 | if (existsSync(absConfigFile)) { 59 | mainConfigFile = absConfigFile; 60 | break; 61 | } 62 | } 63 | return mainConfigFile; 64 | } 65 | static getConfigFiles(opts: { 66 | mainConfigFile: string | null; 67 | env: Env; 68 | specifiedEnv?: string; 69 | }) { 70 | const ret: string[] = []; 71 | const { mainConfigFile } = opts; 72 | const specifiedEnv = opts.specifiedEnv || ''; 73 | if (mainConfigFile) { 74 | const env = SHORT_ENV[opts.env] || opts.env; 75 | ret.push( 76 | ...[ 77 | mainConfigFile, 78 | specifiedEnv && 79 | addExt({ file: mainConfigFile, ext: `.${specifiedEnv}` }), 80 | addExt({ file: mainConfigFile, ext: `.${env}` }), 81 | specifiedEnv && 82 | addExt({ 83 | file: mainConfigFile, 84 | ext: `.${env}.${specifiedEnv}`, 85 | }), 86 | ].filter(Boolean), 87 | ); 88 | 89 | if (opts.env === Env.development) { 90 | ret.push(addExt({ file: mainConfigFile, ext: LOCAL_EXT })); 91 | } 92 | } 93 | return ret; 94 | } 95 | 96 | static getUserConfig(opts: { configFiles: string[] }) { 97 | let config = { 98 | viteConfig: { 99 | 100 | } 101 | }; 102 | let files: string[] = []; 103 | 104 | for (const configFile of opts.configFiles) { 105 | if (existsSync(configFile)) { 106 | register.register({ 107 | implementor: esbuild, 108 | }); 109 | register.clearFiles(); 110 | config = lodash.merge(config, require(configFile).default); 111 | for (const file of register.getFiles()) { 112 | delete require.cache[file]; 113 | } 114 | // includes the config File 115 | files.push(...register.getFiles()); 116 | register.restore(); 117 | } else { 118 | files.push(configFile); 119 | } 120 | } 121 | return { 122 | config, 123 | files, 124 | }; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /手写可插拔前端框架小册/1-3.构建编译-现代前端构建方案.md: -------------------------------------------------------------------------------- 1 | ### 该小节主要知识点: 2 | 3 | - ⭐️ `模块化`的发展史 4 | - `Treeshaking` 基本原理 5 | - ⭐️ `Webpack、Rollup、Vite、Esbuild、Babel、Tsc、Swc` 有什么不同 ? 6 | - ⭐️ 什么是 `Bundle、Bundless、No-Bundle`? 7 | 8 | ## 给大家抛出一个问题: 9 | 10 | > 上一小节我们**使用 `tsc`** 将 我们的 **ts 代码 全部 `转换`成了 js、js.map、d.ts 的代码** 11 | > 请问 这是 **`打包`** 吗?`为什么`? 12 | 13 | 在正式解答什么是 `Bundle、Bundless、No-Bundle` 之前,先带大家回顾一下模块化的`发展史` 14 | 15 | ## 模块化的发展史 16 | 17 | 在那个 HTML 文件里面写所有的 JS、CSS 代码,刀耕火种的年代,**`工程化`** 逐渐兴起 18 | 19 | 优秀的前辈们开始了对 **`工程化`** 以及 **`模块化`** 的探索之路 20 | 21 | ### 一.无模块化标准阶段 22 | 23 | #### 1.文件划分 24 | 25 | 问题:`命名冲突`,script`标签顺序管理困难`,变量全部`定义在全局`-调试困难 26 | 27 | #### 2.命名空间 28 | 29 | 优化命名冲突:每个文件都是引入一个大的对象,解决了大对象下的变量命名冲突 30 | 31 | #### 3.IIFE(`立即执行函数`) 32 | 33 | 目的是为了`解决变量命名冲突` 34 | 35 | ### 二.模块化标准阶段 36 | 37 | #### 1.CJS-CommonJs 38 | 39 | 特点:只支持 `node` 环境,同步,导出变量`可以修改` 40 | 41 | #### 2AMD(Asynchronous Module Definition,异步模块加载机制) 42 | 43 | 代表:requirejs 44 | 45 | #### 3.CMD 和 UMD 46 | 47 | CMD:与 AMD 一样解决异步问题 48 | 49 | UMD:一直`兼容CJS和AMD`的方案 50 | 51 | #### 4.ESM(浏览器原生和服务端支持异步)-`ESModule` 52 | 53 | > 在 node 端 如何使用 ESModule 呢? 54 | 55 | ##### 1.package.json 中 type 改为 module 56 | 57 | ##### 2.文件名后缀为 `mjs` 58 | 59 | 特点:双端支持,异步,导出变量为引用,无法修改 60 | 61 | > 在 浏览器端 如何使用 ESModule 呢 62 | 63 | **script`标签中设置 type='module'` 即可** 64 | 65 | ## CJS 与 ESM 的差异-- Treeshaking 基本原理 66 | 67 | ESModule 模块依赖关系是`确定`的,和运行时的状态`无关` 68 | 69 | 可以进行可靠的 ⭐️ **`静态分析` 也就是 `不执行`里面的 js 代码 `就可以分析得到` 依赖图谱** 70 | 71 | 而 `CommonJs` **require `执行之后`** 我们 **`才可以知道`** 这个模块引用了其他的哪些模块 72 | 73 | 74 | ## Webpack、Rollup、Vite、Esbuild、Babel、Tsc、Swc 有什么不同 75 | 76 | ### 第一类叫做 Bundle 型 77 | 78 | #### Webpack - `Bundle` 79 | 80 | 特点: 81 | 82 | 通过`构建依赖图谱` 最终将产物放到 `main.js` 中 83 | 84 | 对于除 js 之外的其他资源 bundle、transform 能力也同样优异 85 | 86 | #### Rollup - `Bundle` 87 | 88 | 特点: 89 | 90 | 通过`构建依赖图谱` 最终将产物打包到 `main.js` 中 91 | 92 | `大力支持ESModule` 93 | 94 | ### 第二类叫做 No-Bundle 型 95 | 96 | #### Vite: 97 | 98 | #### development 阶段:- `No-Bundle` 99 | 100 | 使用 ESbuild 对 ts、tsx、jsx 文件 进行 transform 进行`平行编译之后` 不 Bundle 101 | 102 | 而是通过`浏览器原生支持 ESModule` 的特点,使用 `script标签` `type = 'module'` 引入 103 | 104 | #### production 阶段: - `Bundle` 105 | 106 | 使用`Rollup`进行`全量Bundle` 将产物打包到 `main.js` 中 107 | 108 | ### 第三类叫做 Bundless 型 有些开源库作者也喜欢叫 `Transform` 型 109 | 110 | #### TSC - typescript compiler 111 | 112 | #### Esbuild 113 | 114 | #### Babel 115 | 116 | #### Swc 117 | 118 | 他们的 **`特点`**是什么:**`编译生成 js 代码`** 119 | 120 | 比如我们上一小节中 将 ts 代码编译成了 js 代码 121 | 122 | 实际上 **上面四个都可以完成代码的`编译/转换`** 达到我们想要的效果 123 | 124 | 但是他们的能力 **`仅限于`** 例如 **`ts js jsx tsx`** ... 而`不包括其他资源`例如:css、图片、字体 ... 125 | 126 | ### 所以什么是 Bundless? 127 | 128 | `Bundless` 即`文件到文件`的构建模式,它不对依赖做任何处理,只对`源码`做`平行编译输出` -- (引自辟起老师) 129 | 130 | 以下是示例: 131 | 132 | ``` 133 | └── src 134 | ├── index.less 135 | ├── index.ts 136 | └── test.js 137 | ``` 138 | 139 | 它会被编译成 140 | 141 | ``` 142 | └── src 143 | ├── index.d.ts 144 | ├── index.js 145 | ├── index.less 146 | └── test.js 147 | ``` 148 | 149 | ## 还记得本小节开头的问题吗? 150 | 151 | 上一小节我们**使用 `tsc`** 将 我们的 **ts 代码 全部 `转换`成了 js、js.map、d.ts 的代码** 152 | 153 | 请问这是 **`打包`** 吗? 154 | 155 | 答案是 -- **`不是`** 156 | 157 | 这是 **`Bundless`** 的方案 158 | 159 | ## 所以 什么是 Bundle、Bundless、No-Bundle? 160 | 161 | 相信你看到这里,对于三种模式已经有了自己的理解了 如果还是一知半解 建议`反复观看前面举的例子` 162 | 163 | ### 总结一下: 164 | 165 | **Bundle** 就是通过 **`分析依赖图谱`** **将所有文件`打入一个文件`** 里面去使用 166 | 167 | **Bundless** 就是**只对 所有`源文件`** 进行 **`平行编译/transform` `不会去分析依赖关系`进行打包** 168 | 169 | **No-Bundle** 例如**Vite** 就是 **利用`Bundless` `编译/transform` 之后**通过 **`浏览器原生支持ESModule`** 的特性使用 **script`标签`引入 `type = 'module'`** 做 development 开发 170 | 171 | ## 小结: 172 | 173 | **本小节我们** 174 | 175 | **1.首先带大家系统回顾了一下`模块化发展`的过程,引出了我们现在最常用的`CommonJs、ESModule、IIFE、UMD`等** 176 | 177 | **2.主要介绍了这个 `Treeshaking` 优异的能力,它是通过 `ESModule` `静态分析` 的特点来实现的** 178 | 179 | **3.通过举例当前工程化领域流行的几个工具,为大家介绍了他们的异同指出它们的`特点`,让大家自行体会`什么是Bundle、Bundless、No-Bundle`** 180 | 181 | **4.总结了什么是 Bundle、Bundless、No-Bundle,即 `Bundle`--通过 `分析依赖图谱 `将所有文件`打入一个文件`,`Bundless`--只对 所有`源文件`进行 `平行编译/transform` `不会去分析依赖关系`进行打包,`No-Bundle`--利用 `Bundless 编译/transform` 之后通过`浏览器原生支持ESModule`的特性使用 script`标签引入 type = 'module'`** 182 | 183 | **下一小节将带着大家将我们的第一个 npm 包发布到 npm 仓库中~** 184 | 185 | ## 小节思考: 186 | 187 | **你能自己表述出来 `什么是 Bundle、Bundless、No-Bundle` 吗?** 188 | -------------------------------------------------------------------------------- /packages/core/src/core.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from "./command" 2 | import { Plugin } from "./plugin" 3 | import { PluginAPi } from "./pluginAPI" 4 | import { Hook } from "./hook" 5 | import { 6 | AsyncSeriesWaterfallHook, 7 | } from '@umijs/bundler-utils/compiled/tapable'; 8 | import { Config, Env } from "./config/config"; 9 | import { UserConfig } from "./types"; 10 | 11 | export enum ApplyPluginsType { 12 | add = 'add', 13 | modify = 'modify', 14 | event = 'event', 15 | } 16 | type Cmmands = { 17 | [key: string]: ICommand 18 | } 19 | 20 | type hooksByPluginId = { 21 | [id: string]: Hook[] 22 | } 23 | 24 | type hooks = { 25 | [key: string]: Hook[] 26 | } 27 | 28 | export class Core { 29 | private opts; 30 | cwd: string; 31 | env: Env; 32 | commands: Cmmands = {}; 33 | plugins: string[] = []; 34 | hooksByPluginId: hooksByPluginId = {}; 35 | hooks: hooks = {}; 36 | pluginMethods: Record = {}; 37 | configManager: Config | undefined = undefined; 38 | userConfig: UserConfig|undefined = undefined; 39 | config: UserConfig | undefined = undefined; 40 | userPlugins: string[]=[]; 41 | applyPlugins: (opts: { 42 | key: string; 43 | type?: ApplyPluginsType; 44 | initialValue?: any; 45 | args?: any; 46 | sync?: boolean; 47 | }) => Promise | null 48 | constructor(opts: { cwd: string, env: Env, presets?: string[], plugins: string[], defaultConfigFiles?: string[] }) { 49 | this.opts = opts 50 | this.cwd = opts.cwd 51 | this.env = opts.env; 52 | this.hooksByPluginId = {} 53 | this.hooks = {} 54 | 55 | this.applyPlugins =function( 56 | opts: { 57 | key: string; 58 | type?: ApplyPluginsType; 59 | initialValue?: any; 60 | args?: any; 61 | sync?: boolean; 62 | } 63 | ) { 64 | const hooks = this.hooks[opts.key] 65 | if (!hooks) { 66 | return opts.initialValue 67 | } 68 | // 读取修改用户配置 69 | const waterFullHook = new AsyncSeriesWaterfallHook(['memo']) 70 | 71 | for (const hook of hooks) { 72 | waterFullHook.tapPromise({ 73 | name: 'tmp' 74 | }, 75 | async (memo: any) => { 76 | const items = await hook.fn(memo, opts.args); 77 | return typeof memo !== 'object' ? items : Array.isArray(memo) ? [...memo, ...items] : { ...memo, ...items }; 78 | }, 79 | ) 80 | } 81 | return waterFullHook.promise(opts.initialValue) 82 | } 83 | } 84 | 85 | async run(opts: { name: string; args?: any }) { 86 | const { name, args = {} } = opts; 87 | // 获取用户配置 88 | 89 | const configManager = new Config({ 90 | cwd: this.cwd, 91 | env: this.env, 92 | defaultConfigFiles: this.opts.defaultConfigFiles, 93 | }); 94 | this.configManager = configManager; 95 | this.userConfig = configManager.getUserConfig().config; 96 | 97 | // init Presets 98 | let { presets, plugins } = this.opts 99 | presets = [require.resolve('./servicePlugin')].concat( 100 | presets || [], 101 | ) 102 | 103 | // userPlugins 要最后挂载 104 | this.userPlugins = plugins 105 | this.plugins = [] 106 | if (presets) { 107 | while (presets.length) { 108 | await this.initPreset({ 109 | preset: new Plugin({ path: presets.shift()! }), 110 | presets, 111 | plugins: this.plugins 112 | }); 113 | } 114 | } 115 | this.plugins.push(...this.userPlugins) 116 | 117 | // init 所有插件 118 | this.plugins.forEach(async plugin => { 119 | await this.initPlugin({ plugin: new Plugin({ path: plugin }) }) 120 | }) 121 | 122 | await this.applyPlugins({ 123 | key: 'onCheck', 124 | }); 125 | 126 | await this.applyPlugins({ 127 | key: 'onStart', 128 | }); 129 | // 获取最终的配置 130 | this.config = await this.applyPlugins({ 131 | key: 'modifyConfig', 132 | initialValue: { ...this.userConfig }, 133 | args: {}, 134 | }); 135 | 136 | await this.applyPlugins({ 137 | key: 'onBuildStart', 138 | }) 139 | 140 | const command = this.commands[name] 141 | await command.fn({ ...args }) 142 | } 143 | 144 | async initPreset(opts: { 145 | preset: Plugin; 146 | presets: string[]; 147 | plugins: string[]; 148 | }) { 149 | 150 | const { presets = [], plugins = [] } = await this.initPlugin({ 151 | plugin: opts.preset, 152 | presets: opts.presets, 153 | plugins: opts.plugins, 154 | }); 155 | 156 | opts.presets.unshift(...(presets || [])); 157 | opts.plugins.push(...(plugins || [])); 158 | } 159 | // 用于执行特定 key 的 hook 相当于发布订阅的 emit 160 | 161 | async initPlugin(opts: { plugin: Plugin, presets?: string[], plugins?: string[] }) { 162 | const pluginApi = new PluginAPi({ service: this, plugin: opts.plugin }) 163 | const proxyPluginAPI = PluginAPi.proxyPluginAPI({ 164 | service: this, 165 | pluginAPI: pluginApi, 166 | serviceProps: [ 167 | 'appData', 168 | 'applyPlugins', 169 | 'args', 170 | 'cwd', 171 | 'userConfig', 172 | 'config' 173 | ], 174 | staticProps: { 175 | ApplyPluginsType, 176 | service: this, 177 | }, 178 | }); 179 | return opts.plugin.apply()(proxyPluginAPI) || {} 180 | } 181 | 182 | 183 | 184 | } 185 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/ssg.ts: -------------------------------------------------------------------------------- 1 | import { type ICoreApi } from '@mini-umi/core' 2 | import { 3 | chalk, 4 | winPath, 5 | } from '@umijs/utils'; 6 | import { IpresetUmi } from '../types'; 7 | import { build as viteBuild } from 'vite' 8 | import { join, resolve } from 'path' 9 | 10 | 11 | const isTest = process.env.VITEST 12 | import vuePlugin from '@vitejs/plugin-vue' 13 | import vueJsx from '@vitejs/plugin-vue-jsx' 14 | const virtualFile = '@virtual-file' 15 | const virtualId = '\0' + virtualFile 16 | const nestedVirtualFile = '@nested-virtual-file' 17 | const nestedVirtualId = '\0' + nestedVirtualFile 18 | 19 | const base = './' 20 | 21 | // preserve this to test loading __filename & __dirname in ESM as Vite polyfills them. 22 | // if Vite incorrectly load this file, node.js would error out. 23 | globalThis.__vite_test_filename = __filename 24 | globalThis.__vite_test_dirname = __dirname 25 | export default (api: ICoreApi & IpresetUmi) => { 26 | const cwd = process.cwd() 27 | api.registerCommand({ 28 | name: 'ssg', 29 | async fn() { 30 | 31 | const viteConfig = { 32 | base, 33 | plugins: [ 34 | vuePlugin(), 35 | vueJsx(), 36 | { 37 | name: 'virtual', 38 | resolveId(id) { 39 | if (id === '@foo') { 40 | return id 41 | } 42 | }, 43 | load(id, options) { 44 | const ssrFromOptions = options?.ssr ?? false 45 | if (id === '@foo') { 46 | // Force a mismatch error if ssrBuild is different from ssrFromOptions 47 | return `export default { msg: '${command === 'build' && !!ssrBuild !== ssrFromOptions 48 | ? `defineConfig ssrBuild !== ssr from load options` 49 | : 'hi' 50 | }' }` 51 | } 52 | } 53 | }, 54 | { 55 | name: 'virtual-module', 56 | resolveId(id) { 57 | if (id === virtualFile) { 58 | return virtualId 59 | } else if (id === nestedVirtualFile) { 60 | return nestedVirtualId 61 | } 62 | }, 63 | load(id) { 64 | if (id === virtualId) { 65 | return `export { msg } from "@nested-virtual-file";` 66 | } else if (id === nestedVirtualId) { 67 | return `export const msg = "[success] from conventional virtual file"` 68 | } 69 | } 70 | }, 71 | // Example of a plugin that injects a helper from a virtual module that can 72 | // be used in renderBuiltUrl 73 | (function () { 74 | const queryRE = /\?.*$/s 75 | const hashRE = /#.*$/s 76 | const cleanUrl = (url) => url.replace(hashRE, '').replace(queryRE, '') 77 | let config 78 | 79 | const virtualId = '\0virtual:ssr-vue-built-url' 80 | return { 81 | name: 'built-url', 82 | enforce: 'post', 83 | configResolved(_config) { 84 | config = _config 85 | }, 86 | resolveId(id) { 87 | if (id === virtualId) { 88 | return id 89 | } 90 | }, 91 | load(id) { 92 | if (id === virtualId) { 93 | return { 94 | code: `export const __ssr_vue_processAssetPath = (url) => '${base}' + url`, 95 | moduleSideEffects: 'no-treeshake' 96 | } 97 | } 98 | }, 99 | transform(code, id) { 100 | const cleanId = cleanUrl(id) 101 | if ( 102 | config.build.ssr && 103 | (cleanId.endsWith('.js') || cleanId.endsWith('.vue')) && 104 | !code.includes('__ssr_vue_processAssetPath') 105 | ) { 106 | return { 107 | code: 108 | `import { __ssr_vue_processAssetPath } from '${virtualId}';__ssr_vue_processAssetPath;` + 109 | code, 110 | sourcemap: null // no sourcemap support to speed up CI 111 | } 112 | } 113 | } 114 | } 115 | })() 116 | ], 117 | experimental: { 118 | renderBuiltUrl(filename, { hostType, type, ssr }) { 119 | if (ssr && type === 'asset' && hostType === 'js') { 120 | return { 121 | runtime: `__ssr_vue_processAssetPath(${JSON.stringify(filename)})` 122 | } 123 | } 124 | } 125 | }, 126 | build: { 127 | minify: false 128 | }, 129 | ssr: { 130 | noExternal: [ 131 | // this package has uncompiled .vue files 132 | 'example-external-component' 133 | ] 134 | }, 135 | optimizeDeps: { 136 | exclude: ['example-external-component'] 137 | }, 138 | resolve: { 139 | alias: { 140 | '@': '../' 141 | } 142 | } 143 | } 144 | await viteBuild({ 145 | ...viteConfig, 146 | configFile:false, 147 | base: './', 148 | build: { 149 | ssrManifest: true, 150 | rollupOptions: { 151 | input: resolve(cwd, './.mini-umi-ssr/index.html'), 152 | output: { 153 | dir: resolve(cwd, './.mini-umi-ssr/static') 154 | }, 155 | } 156 | } 157 | }) 158 | 159 | await viteBuild({ 160 | ...viteConfig, 161 | configFile:false, 162 | base: './', 163 | build: { 164 | rollupOptions: { 165 | input: resolve(cwd, './.mini-umi-ssr/entry-server.js'), 166 | 167 | }, 168 | outDir: resolve(cwd, './.mini-umi-ssr/server'), 169 | } 170 | }) 171 | console.log(); 172 | console.log(); 173 | console.log(); 174 | 175 | console.log( 176 | chalk.greenBright('----------构建产物成功-----------') 177 | ); 178 | console.log(); 179 | console.log(); 180 | console.log( 181 | `${chalk.yellowBright('请使用')} ${chalk.blueBright('npm run preview')} ${chalk.yellowBright('预览')}` 182 | ); 183 | console.log(); 184 | console.log(); 185 | } 186 | }) 187 | } 188 | -------------------------------------------------------------------------------- /手写可插拔前端框架小册/1-2.牛刀小试-实现并发布一个CLI.md: -------------------------------------------------------------------------------- 1 | #### 该小节主要知识点: 2 | 3 | - npm 包项目搭建 4 | - 认识类库开发时常用 `package.json` 字段 5 | - 正确使用 `bin` 字段 6 | - 什么是 `npm link` 7 | - 带大家一起发布第一个包 8 | 9 | 10 | 11 | > 本小节所有代码已放入 [mini-umi](https://github.com/BoyYangzai/mini-umi) 仓库 `/juejin/cli`目录 下 12 | 13 | 这里我带大家一步一步来实现我们的第一个 npm 包 14 | 15 | 请对 `npm包`或 `CLI开发` 不熟悉的小伙伴 `拿起鼠标键盘`跟着我一起完成 16 | 17 | ## 1.项目搭建 18 | 19 | ### 安装 pnpm 20 | 21 | **pnpm 比 npm** **`更快更强大` 放心用** 22 | 23 | ```js 24 | npm i pnpm -g // 全局安装 pnpm 25 | ``` 26 | 27 | ### 安装 TypeScript 28 | 29 | 优秀的`类型提示`可以给使用者极佳的体验 30 | 31 | ```js 32 | npm i typescript -d //我个人比较喜欢全局安装 33 | ``` 34 | 35 | ### 创建项目 36 | 37 | 打开终端: 不熟悉终端操作的小伙伴可手动操作 38 | 39 | ```js 40 | mkdir first-pkg //创建文件夹 41 | code first-pkg // 打开vscode并进入该项目 42 | ``` 43 | 44 | ### 初始化项目 45 | 46 | 生成 `package.json` 47 | 48 | ```js 49 | pnpm init //初始化并自动生成package.json 50 | ``` 51 | 52 | ```diff 53 | +{ 54 | + "name": "first-pkg", 55 | + "version": "1.0.0", 56 | + "description": "", 57 | + "main": "index.js", 58 | + "scripts": { 59 | + "test": "echo "Error: no test specified" && exit 1" 60 | + }, 61 | + "keywords": [], 62 | + "author": "", 63 | + "license": "ISC" 64 | +} 65 | ``` 66 | 67 | ### 生成 tsconfig.json 68 | 69 | ```js 70 | tsc --init 71 | ``` 72 | 73 | ### 创建 src/index.ts 74 | 75 | 现在项目的目录结构是这样: 76 |  77 | 78 | ### 加入以下代码: 79 | 80 | ```ts 81 | // src/index.ts 82 | export const yang = 'yang' 83 | console.log(yang) 84 | ``` 85 | 86 | ### 修改 tsconfig.json 87 | 88 | 先配置几个需要的 89 | 90 | ```json 91 | { 92 | "compilerOptions": { 93 | "target": "es2016", // 编译产物 94 | "module": "commonjs", 95 | "moduleResolution": "node", // 模块声明 96 | "baseUrl": "./", 97 | "declaration": true, // 生成 d.ts 文件 98 | "sourceMap": true, // 开启 sourcemap 方便打断点调试源文件 99 | "outDir": "dist", // 编译后输出文件夹 100 | "declarationDir": "dist", 101 | "esModuleInterop": true, 102 | "forceConsistentCasingInFileNames": true, 103 | "strict": true, 104 | "skipLibCheck": true 105 | } 106 | } 107 | ``` 108 | 109 | ### 修改 package.json 110 | 111 | ```diff 112 | { 113 | "name": "first-pkg", 114 | "version": "1.0.0", 115 | "description": "", 116 | + "main": "/dist/index.js", 117 | - "test": "echo "Error: no test specified" && exit 1" 118 | "scripts": { 119 | + "dev": "tsc --watch", // --watch表示文件修改后自动重新编译 120 | + "build": "tsc --target es5" 121 | }, 122 | "keywords": [], 123 | "author": "", 124 | "license": "ISC" 125 | } 126 | ``` 127 | 128 | ### 测试脚本是否可用 129 | 130 | ```js 131 | npm run dev 132 | ``` 133 | 134 | 成功生成 `dist`目录 如下则说明成功~ 135 | 136 |  137 | 138 | 到这里,一个`项目雏形`就搭建好啦~ 139 | 140 | ## 2.认识`类库开发`时常用 `package.json 字段` 141 | 142 | 下面以 [mini-umi](https://github.com/BoyYangzai/mini-umi) 这个包中的 package.json 为例: 143 | 144 | ### ⭐️ name 145 | 146 | 发到 npm 的`包名`,也是其他用户下载时的名字 147 | 148 | > 注意:包名与当前目录的文件夹名无关 149 | 150 | ### ⭐️ version 151 | 152 | 发布的`版本号` 版本号这里有一套详细的规则 153 | 154 | 一般是三位 X.Y.Z 155 | 156 | X 是大版本 一般只有`大版本更新`产生大量 `breakchanges` 之后才会上 比如 Vue2-Vue3 157 | 158 | 我们在安装依赖的时候一般都是 --- `'^X.Y.Z'` 159 | 160 | ^表示:在安装的时候 如果没有 lock 文件 都会`自动安装`当前 `X大版本下的最新小版本` 161 | 162 | ### description 163 | 164 | 这个 npm 包的`描述`,可以在 npm 仓库里面展示 165 | 166 | ### ⭐️ main 167 | 168 | 别人在使用这个 npm 包时 通过直接 import '包名' 时`默认指向`这个文件 169 | 170 | 也就是别人使用我们这个包时 `访问的入口文件` 171 | 172 | ### ⭐️ types 173 | 174 | d.ts `声明文件的入口` 175 | 176 | ### ⭐️ ⭐️ bin 177 | 178 | 这个 npm 包 的 `bin 脚本` ⭐️ ⭐️ 非常重要 接下来会带大家去使用 bin 字段 179 | 180 | ### ⭐️ script 181 | 182 | 设置`当前`工作区的脚本 183 | **`设置之后`使用 `npm run xx` 会`优先使用`在`当前工作区`的 `node_modules`** 184 | 185 | ### ⭐️ dependencies 186 | 187 | 项目的依赖 `生产环境`依然有效 188 | 189 | ### ⭐️ devDependencies 190 | 191 | 项目的本地依赖 只有开发环境生效 生产环境 `build`的时候`不会生效` 192 | 193 | 通常安装像 typescript、@types/xxx 这种只有`开发阶段起作用`的库 194 | 195 | ### ⭐️⭐️ files 196 | 197 | 在发布这个 npm 包 的时候 `上传`的所有`文件夹` 198 | 199 | 通常情况下都是默认 dist 200 | 201 | tips: 后续开发中不仅仅需要上传 dist 202 | 203 | 也就是说我们在发布这个 npm 包的时候 可能不仅仅只有 dist 目录以及 package.json 会被上传 204 | 205 | ### repository 206 | 207 | 在 npm 仓库中可以展示`链接`的 Github 仓库 208 | 209 | ### keywords 210 | 211 | `关键词` 例如: [ "umi","umijs","mini-umi" ] 212 | 213 | ### author 214 | 215 | 仓库的`作者` 216 | 217 | ### license 218 | 219 | `开源协议`:默认为"ISC" 220 | 221 | 222 | 最终填写效果展示如下: 223 | 224 | ```json 225 | { 226 | "name": "mini-umi", 227 | "version": "1.3.5", 228 | "description": "a simple model for Umi and Vue3.2 + Vite", 229 | "main": "dist/index.js", 230 | "types": "dist/index.d.ts", 231 | "bin": { 232 | "mumi": "bin/mini-umi.js" 233 | }, 234 | "scripts": { 235 | "dev": "father dev", 236 | "build": "father build", 237 | "publish:all": "pnpm publish --no-git-checks", 238 | "build:deps": "father prebundle", 239 | "prepublishOnly": "father doctor && npm run build" 240 | }, 241 | "dependencies": { 242 | "@mini-umi/preset-example": "workspace:*", 243 | "@mini-umi/preset-umi": "workspace:*", 244 | "@mini-umi/core": "workspace:*", 245 | "@umijs/utils": "^4.0.3" 246 | }, 247 | "files": ["bin", "dist"], 248 | "repository": "https://github.com/BoyYangzai/mini-umi", 249 | "keywords": ["umi", "umijs", "mini-umi"], 250 | "author": "洋", 251 | "license": "ISC" 252 | } 253 | ``` 254 | 255 | 到这里需要单独把 `bin` 字段给提出来给大家演示一下 256 | 257 | ## 3.正确使用 `bin 字段` 258 | 259 | 接下来回到我们的 first-pkg 项目中 260 | 261 | ### 新建 bin 字段 262 | 263 | ```diff 264 | + "bin": { 265 | + "yang": "./bin/yang.js" 266 | + }, 267 | ``` 268 | 269 | 意思就是 当你 在终端输入 yang 的时候他会`自动执行` './bin/yang.js' 文件里的代码 270 | 271 | ### 新建 bin 目录 在该目录下`新建 yang.js` 272 | 273 | 将以下代码复制到文件中 274 | 275 | ```diff 276 | bin/yang.js 277 | + #!/usr/bin/env node 278 | + console.log('我是洋'); 279 | ``` 280 | 281 | tips: 282 | 283 | #!/usr/bin/env node 表示我们会使用`node环境`执行以下代码 这是必须要有的 284 | 285 | 这样 当我们 **在终端输入 yang 的时候他会自动执行 './bin/yang.js' 里的代码** 286 | 287 | ### 在终端尝试输入 288 | 289 | ``` 290 | yang 291 | ``` 292 | 293 |  294 | 295 | 很明显 **失败了** **`command not found`** 296 | 297 | **不要慌** 298 | 299 | 我们执行这个命令 300 | 301 | ``` 302 | npm link 303 | ``` 304 | 305 |  306 | 307 | 在终端上`成功输出`了--’我是洋‘ 308 | 309 | 可是为什么一定要有 npm link 才会成功呢? 310 | 311 | ## 4.什么是 `npm link` 呢? 312 | 313 | 简单说就是为`当前开发`的这个包创造一个 **`全局软链接`** 314 | 315 | --- 316 | 317 | 你可以使用` bin 命令` 是因为当你在**安装这个类库**时 318 | 319 | 你的 `node_modules` 下会 `自动创建` 一个 **`.bin 目录`** 320 | 321 |  322 | 当你去执行 比方说 **esbuild** 这个命令的时候 他会去**自动执行 .bin 目录下特定的代码** 323 | 324 | --- 325 | 326 | 但是 327 | 328 | 我们 **`本地的 first-pkg`** 并 `没有作为` `node_modules` 安装在**我们的项目**里 329 | 330 | 自然也就出现了 **`command not found`** 331 | 332 | --- 333 | 334 | 当你在 first-pkg 这个 路径下 使用 `npm link`的时候 335 | 336 | first-pkg 这个包就会 **`被链接到全局`** 337 | 338 | **相当于 你 `npm i first-pkg -g`** 339 | 340 | 而且你`随时修改` first-pkg 里面的代码 全局的 first-pkg 都**会`随着更新`** 341 | 342 | 因为他 **`本质`上是个`链接` 链接尽头**还**是我们这个包** 343 | 344 | --- 345 | 346 | **npm link 是一个非常`强大`的功能** 347 | 348 | 当我们在 B 包里面想使用 A 包时必须 npm i A 包,那我们本地就需要先把 A 包发布到 npm 仓库里,但是 我想开发 B 包的同时也修改 A 包里面的内容该怎么办呢? 349 | 350 | 我这里是一个 Vue3 项目=B 包 引入了 Element-plus=A 包 351 | 352 | 但是我想在 Vue3 里`使用`的是我 **`本地克隆`下来的 Element-plus** 353 | 354 | 这样我就能**在 Element-plus 这个项目中 `修改代码并生效`** 在我的 Vue3 项目里啦 然后 修复某个组件的 Issue 并提交 pr 美滋滋 355 | 356 | (当然 大多数专业的类库都会在自己的仓库中使用 monorepo 配备自己的 `example`,上述只是举个例子) 357 | 358 | 像 [Element-plus](https://github.com/element-plus/element-plus) 仓库中的 **play** **这个包** 其实就是我们这里的 **Vue3(B 包)** 359 | 360 | **package 下的包** 就是我们的 **Element-plus 本体 也就是 A 包** 361 | 362 | 只不过 A 包 B 包在同一个仓库里通过 monorepo 的方式管理 通过 **`workspace`** 去进行 **`软链接` 而不是 npm link** 363 | 364 | 说白了 **`monorepo`** 是一种 **`仓库管理模式`** 365 | 366 | monorepo 仓库可以通过 **workspace** 达到 **究极`加强版`** `npm link` **而不是本地 link 类库开发的方式** 367 | 368 | 369 | 370 | ## 5.发布我们的 npm 包 371 | 372 | ### 1.注册 npm 账号 373 | 374 | 在[npm 官网](https://www.npmjs.com/)点击 `Sign up`,根据提示操作完成账号注册 375 | 376 | ![]() 377 | 378 | ### 2.`开发`我们的 npm 包 379 | 380 | 根据前面的步骤完成我们的 npm 包 搭建与开发 ~ 381 | 382 | ### 3.IDE 绑定 npm 账号 383 | 384 | 打开终端,执行 `npm login` 登录,按照提示填写对应的内容 385 | 386 | 依次是用户名、密码、邮箱,可能还会有一次性邮箱验证码 387 | 388 | ### 4.在终端输入命令发布公有包,可以被全世界下载 389 | 390 | ``` 391 | npm publish --access public 392 | ``` 393 | 394 | 395 | 396 | ## 6.现在我们一起发布一下 first-pkg 这个包 397 | 398 | - 1.注册账号 ✅ 399 | 400 | - 2.开发 npm 包 ✅ 401 | 402 | - 3.绑定账号 ✅ 403 | 404 | 就剩下第四步`npm publish`了 405 | 406 | ### 检查一下 package.json 407 | 408 |  409 | 410 | - 包名符合规范 ✅ 411 | 412 | tips:@开头的包必须加入对应的`组织` 比如@Vue/xxx 你就必须在 Vue 组织里才有发包`权限` 413 | 414 | - 版本号 ok ✅ 415 | 416 | - 入口文件 我们这里没有用到 ✅ 417 | 418 | - bin 脚本 ✅ 419 | 420 | - 作者信息等 ✅ 421 | 422 | 这里有个问题----`没有files字段`,因为我们要执行 bin 文件夹里面的内容 所以需要加上 `files` 字段`上传bin目录` 423 | 424 | ```diff 425 | //package.json 426 | + "files":[ 427 | + "bin" 428 | + ] 429 | ``` 430 | 431 | ### 发包!-执行 npm publish --access public 432 | 433 |  434 | 435 | 很好,失败了 看下日志 原因可能是这个名字早就被占用了 我们换个包名 436 | 437 |  438 | 439 | 重新执行 npm publish --access public 440 | 441 |  442 | 443 | 成功了! 444 | 445 | ### 我们验证一下是否 开发并发包成功 446 | 447 | ### 首先执行 以下命令 取消 我们这个包的 全局软链接 link 448 | 449 | > tips: 为什么要有 -g ,因为它是 npm link 是`软链接到全局` 450 | > 不信你执行 `npm ls -g` 看看是不是有它 451 | 452 | ``` 453 | npm unlink first-pkg -g 454 | ``` 455 | 456 | 这样我们输入 yang 就 `command not found` 了 457 | 458 |  459 | 460 | ### 现在我们去新的终端执行以下命令 : 461 | 462 | ``` 463 | npx yang-first-pkg 464 | ``` 465 | 466 | ## 什么是 npx? 467 | 468 | npx 会在当前目录下的./node_modules/.bin 里去查找是否有可执行的命令,没有找到的话再从全局里查找是否有安装对应的模块,全局也没有的话就会自动下载对应的模块,并且用完立即删除,所以用来`执行CLI`再好不过了 469 | 470 |  471 | 472 | (1.0.1 是因为第一次上传`忘记上传` `files` 字段了哈哈哈 重新发了一次包) 473 | 474 | ### 目标完成 成功~ 475 | 476 | 477 | 478 | ## 小结: 479 | 480 | **本小节我们** 481 | 482 | **1.首先搭建了基本的项目 修改了 tsconfig.json 配置和 package.json 的 script ,完成了使用 `tsc 编译` ts 代码、生成 js 产物文件的功能。** 483 | 484 | **2.为大家介绍了在进行类库开发时常用的一些 `package.json 字段` 例如:bin、files 等** 485 | 486 | **3.详细展开为大家介绍了 bin 脚本,使用 bin 脚本编写了第一个 CLI ,并讲解了 `CLI 不能顺利执行的原因`** 487 | 488 | **4.接下来我们 引入了强大的 `npm link` 功能,顺利执行我们的 CLI 并 介绍了 npm link 是什么,以及举例 Element-plus 仓库 的 play 与 package 两个包,说明 `monorepo` 中 `workspace` 与 npm link 的关系** 489 | 490 | **5.带着大家学习了 npm 包 的`发布流程`,成功发布了我们的第一个 npm 包** 491 | 492 | **下一小节将为大家介绍工程化中的 `Bundle、Bundless、No-Bundle` 与常见的生态工具~** 493 | 494 | ## 小节思考: 495 | 496 | 你能回答的出来 `执行 npm run xxx 之后发生了什么` 吗? 497 | -------------------------------------------------------------------------------- /packages/preset-umi/src/commands/ssr.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import fs from 'fs' 3 | import path from 'path' 4 | import express from 'express' 5 | import { winPath, deepmerge, BaseGenerator, chokidar, chalk, fsExtra } from '@umijs/utils' 6 | import http from 'http' 7 | const isTest = process.env.VITEST 8 | import vuePlugin from '@vitejs/plugin-vue' 9 | import vueJsx from '@vitejs/plugin-vue-jsx' 10 | import { getRoutes } from './getRoutes.js' 11 | import { getRoutesString } from './utils.js' 12 | const virtualFile = '@virtual-file' 13 | const virtualId = '\0' + virtualFile 14 | const nestedVirtualFile = '@nested-virtual-file' 15 | const nestedVirtualId = '\0' + nestedVirtualFile 16 | 17 | const base = './' 18 | 19 | // preserve this to test loading __filename & __dirname in ESM as Vite polyfills them. 20 | // if Vite incorrectly load this file, node.js would error out. 21 | globalThis.__vite_test_filename = __filename 22 | globalThis.__vite_test_dirname = __dirname 23 | 24 | 25 | 26 | 27 | export default (api: any) => { 28 | api.registerCommand({ 29 | name: 'ssr', 30 | async fn() { 31 | let vite; 32 | const cwd = process.cwd() 33 | async function resolveRoutes() { 34 | const routesDirPath = await api.applyPlugins({ 35 | key: 'modifyRoutesDir', 36 | initialValue: api.userConfig!.routesDir 37 | }) 38 | const userRoutes = api.userConfig.routes ? api.userConfig.routes : [] 39 | 40 | const routes = deepmerge(getRoutes({ 41 | dirPath: routesDirPath || './pages' 42 | }), userRoutes) 43 | return getRoutesString(routes) 44 | } 45 | 46 | // layout/index.vue 47 | function layout() { 48 | try { 49 | const layoutContent = fsExtra.readFileSync(winPath(path.join(cwd, './layout/index.vue')), 'utf-8') 50 | fsExtra.writeFileSync(winPath(path.join(cwd, './.mini-umi-ssr/App.vue')), layoutContent) 51 | } catch (err) { 52 | console.log(`${err}: ${cwd}`); 53 | } 54 | } 55 | 56 | let hmrPort = 8006 57 | async function createServer( 58 | root = process.cwd(), 59 | isProd = process.env.NODE_ENV === 'production', 60 | 61 | ) { 62 | const generate = new BaseGenerator({ 63 | path: winPath(path.join(__dirname, '../../ssrtemplate')), 64 | target: path.join(cwd, './.mini-umi-ssr/'), 65 | data: { 66 | routes: await resolveRoutes() 67 | }, 68 | questions: [] 69 | }) 70 | await generate.run() 71 | 72 | // generate layout 73 | layout() 74 | 75 | const resolve = (p) => path.resolve(process.cwd(), p) 76 | const indexProd = isProd 77 | ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8') 78 | : '' 79 | 80 | const manifest = isProd 81 | ? JSON.parse( 82 | fs.readFileSync(resolve('dist/client/ssr-manifest.json'), 'utf-8') 83 | ) 84 | : {} 85 | 86 | // start dev server 87 | const app = express() 88 | const defaultViteConfig = { 89 | base, 90 | plugins: [ 91 | vuePlugin(), 92 | vueJsx(), 93 | { 94 | name: 'virtual', 95 | resolveId(id) { 96 | if (id === '@foo') { 97 | return id 98 | } 99 | }, 100 | load(id, options) { 101 | const ssrFromOptions = options?.ssr ?? false 102 | if (id === '@foo') { 103 | // Force a mismatch error if ssrBuild is different from ssrFromOptions 104 | return `export default { msg: '${command === 'build' && !!ssrBuild !== ssrFromOptions 105 | ? `defineConfig ssrBuild !== ssr from load options` 106 | : 'hi' 107 | }' }` 108 | } 109 | } 110 | }, 111 | { 112 | name: 'virtual-module', 113 | resolveId(id) { 114 | if (id === virtualFile) { 115 | return virtualId 116 | } else if (id === nestedVirtualFile) { 117 | return nestedVirtualId 118 | } 119 | }, 120 | load(id) { 121 | if (id === virtualId) { 122 | return `export { msg } from "@nested-virtual-file";` 123 | } else if (id === nestedVirtualId) { 124 | return `export const msg = "[success] from conventional virtual file"` 125 | } 126 | } 127 | }, 128 | // Example of a plugin that injects a helper from a virtual module that can 129 | // be used in renderBuiltUrl 130 | (function () { 131 | const queryRE = /\?.*$/s 132 | const hashRE = /#.*$/s 133 | const cleanUrl = (url) => url.replace(hashRE, '').replace(queryRE, '') 134 | let config 135 | 136 | const virtualId = '\0virtual:ssr-vue-built-url' 137 | return { 138 | name: 'built-url', 139 | enforce: 'post', 140 | configResolved(_config) { 141 | config = _config 142 | }, 143 | resolveId(id) { 144 | if (id === virtualId) { 145 | return id 146 | } 147 | }, 148 | load(id) { 149 | if (id === virtualId) { 150 | return { 151 | code: `export const __ssr_vue_processAssetPath = (url) => '${base}' + url`, 152 | moduleSideEffects: 'no-treeshake' 153 | } 154 | } 155 | }, 156 | transform(code, id) { 157 | const cleanId = cleanUrl(id) 158 | if ( 159 | config.build.ssr && 160 | (cleanId.endsWith('.js') || cleanId.endsWith('.vue')) && 161 | !code.includes('__ssr_vue_processAssetPath') 162 | ) { 163 | return { 164 | code: 165 | `import { __ssr_vue_processAssetPath } from '${virtualId}';__ssr_vue_processAssetPath;` + 166 | code, 167 | sourcemap: null // no sourcemap support to speed up CI 168 | } 169 | } 170 | } 171 | } 172 | })() 173 | ], 174 | experimental: { 175 | renderBuiltUrl(filename, { hostType, type, ssr }) { 176 | if (ssr && type === 'asset' && hostType === 'js') { 177 | return { 178 | runtime: `__ssr_vue_processAssetPath(${JSON.stringify(filename)})` 179 | } 180 | } 181 | } 182 | }, 183 | build: { 184 | minify: false 185 | }, 186 | ssr: { 187 | noExternal: [ 188 | // this package has uncompiled .vue files 189 | 'example-external-component' 190 | ] 191 | }, 192 | optimizeDeps: { 193 | exclude: ['example-external-component'] 194 | }, 195 | resolve: { 196 | alias: { 197 | '@': '../' 198 | } 199 | } 200 | } 201 | const userViteConfig = await api.applyPlugins({ 202 | key: 'modifyViteConfig', 203 | initialValue: api.config!.viteConfig 204 | }) 205 | const viteConfig = deepmerge(userViteConfig, defaultViteConfig) 206 | /** 207 | * @type {import('vite').ViteDevServer} 208 | */ 209 | if (!isProd) { 210 | vite = await ( 211 | await import('vite') 212 | //@ts-ignore 213 | ).createServer({ 214 | ...viteConfig, 215 | configFile: false, 216 | base: './', 217 | root, 218 | logLevel: isTest ? 'error' : 'info', 219 | server: { 220 | middlewareMode: true, 221 | watch: { 222 | // During tests we edit the files too fast and sometimes chokidar 223 | // misses change events, so enforce polling for consistency 224 | usePolling: true, 225 | interval: 100 226 | }, 227 | hmr: { 228 | port: hmrPort 229 | } 230 | }, 231 | appType: 'custom' 232 | }) 233 | // use vite's connect instance as middleware 234 | app.use(vite.middlewares) 235 | } else { 236 | app.use((await import('compression')).default()) 237 | app.use( 238 | '/test/', 239 | (await import('serve-static')).default(resolve('dist/client'), { 240 | index: false 241 | }) 242 | ) 243 | } 244 | 245 | app.use('*', async (req, res) => { 246 | try { 247 | // const url = req.originalUrl.replace('/test/', '/') 248 | 249 | let template, render 250 | if (!isProd) { 251 | // always read fresh template in dev 252 | template = fs.readFileSync(resolve('./.mini-umi-ssr/index.html'), 'utf-8') 253 | template = await vite.transformIndexHtml(req.originalUrl, template) 254 | render = (await vite.ssrLoadModule(path.join(process.cwd(), '/.mini-umi-ssr/entry-server.js'))).render 255 | } else { 256 | template = indexProd 257 | // @ts-ignore 258 | render = (await import('./dist/server/entry-server.js')).render 259 | } 260 | 261 | const [appHtml, preloadLinks] = await render(req.originalUrl, manifest) 262 | 263 | const html = template 264 | .replace(``, preloadLinks) 265 | .replace(``, appHtml) 266 | 267 | res.status(200).set({ 'Content-Type': 'text/html' }).end(html) 268 | } catch (e) { 269 | vite && vite.ssrFixStacktrace(e) 270 | console.log(e.stack) 271 | res.status(500).end(e.stack) 272 | } 273 | }) 274 | 275 | return { app, vite } 276 | } 277 | const { app } = await createServer() 278 | let server; 279 | if (!isTest) { 280 | server = http.createServer(app) 281 | server.listen(8005, () => { 282 | console.log(); 283 | console.log(); 284 | console.log( 285 | chalk.greenBright('🎉🎉🎉恭喜你,mini-umi + Vue3.2 + Vite 启动成功!'), 286 | ); 287 | console.log(); 288 | console.log(); 289 | 290 | console.log( 291 | chalk.blueBright(' . 🎉 SSR模式启动---------------------- ') 292 | ); 293 | console.log( 294 | chalk.yellowBright(' . 🎉 请访问host: http://localhost:8005') 295 | ); 296 | console.log( 297 | chalk.redBright(' . 🎉 请访问host: http://localhost:8005') 298 | ); 299 | 300 | console.log(); 301 | console.log(); 302 | }) 303 | } 304 | 305 | // page route 热更新 306 | chokidar.watch(path.join(cwd, './pages'), { 307 | ignoreInitial: true, 308 | }).on('all', async () => { 309 | await resolveRoutes() 310 | }) 311 | console.log(path.join(cwd, './layout')); 312 | 313 | // layout 重新生成 314 | chokidar.watch(path.join(cwd, './layout'), { 315 | ignoreInitial: true, 316 | }).on('all', async () => { 317 | hmrPort++; 318 | await createServer() 319 | }) 320 | 321 | } 322 | }) 323 | } 324 | -------------------------------------------------------------------------------- /手写可插拔前端框架小册/1-5.源码调试-脚本断点调试.md: -------------------------------------------------------------------------------- 1 | #### 该小节主要知识点: 2 | 3 | - 为什么需要调试 `bin 脚本`? 4 | - 如何快速`上手`调试 bin 脚本 5 | - 什么是 `sourcemap` ?调试中为什么需要 sourcemap ? 6 | - 实战:调试 `Umi 源代码` 7 | - 基于调试中的问题,给 Umi 提个 **pr** 8 | 9 | ### 本小节最终效果: 10 | 11 | (这是我手写 mini-umi 时在 **`Umi 源码`** 中打的所有 `关键断点` ) 12 |  13 | 14 | ## 1.为什么需要调试 bin 脚本? 15 | 16 | 你是否有思考过,当你在使用 VueCLI 或 create-react-app 创建的模版项目中使用 `npm run dev` 时,Webpack 是如何 从 0 到 1 启动这整个项目的吗? 17 | 18 | 当你在使用 Vue3+Vite 项目时,Vite dev 命令到底做了什么事情呢?Vite dev 与 Webpack 的 dev 又有什么不一样呢? 19 | 20 | 除了 Webpack,Vite,更多的还有 `Nuxt`、`Next`、`Esbuild`等框架或工具 21 | 22 | 除了 dev,还有 `build`、`lint`、`test`等指令 23 | 24 | 如果你想知道他们工作以及运转的原理,就需要阅读他们的源代码,而通过调试 CLI 入 口的 `bin脚本`,就可以看到所有指令 **`从0到1`** 是如何 **`执行`** 的 25 | 26 | ## 2.如何快速上手调试 bin 脚本 27 | 28 | **bin 脚本的调试其实是非常`简单`的,我们来简单盘一下逻辑** 29 | 30 | 还记得我们在第 4 小节手写 CLI 时学过:如果你是一个 Node 侧的命令行工具,当你去执行 `npm run xxx` 时,其实都是去执行了 bin 目录下的脚本文件 31 | 32 | 而 Node 环境的`脚本`本质上不过是一个 **`Nodejs 文件`** 33 | 34 | 所以,我们调试 bin 脚本 和调试运行一个 Nodejs 文件又有什么区别呢? 35 | 36 | #### 那如何调试一个 Nodejs 文件呢? 37 | 38 | 接下来我带着大家一步一步尝试: 39 | 40 | #### 1.创建我们的调试项目 41 | 42 | ``` 43 | mkdir debugger 44 | code debugger 45 | ``` 46 | 47 | #### 2.创建调试的入口文件 48 | 49 | ```ts 50 | // debugger.js 51 | const yang = 'yang' 52 | 53 | const yangyang = yang + 'yang' 54 | 55 | console.log(yangyang + 'yang') 56 | ``` 57 | 58 | #### 3.尝试运行 59 | 60 | ``` 61 | node debugger.js 62 | // yangyangyang 63 | ``` 64 | 65 | 运行成功 ✅ 66 | 67 | ### 接下来开始调试的部分 68 | 69 | #### 1.进入 vscode 调试面板 70 | 71 |  72 | 73 | #### 2.给我们的 debugger.js 文件打一个断点 74 | 75 |  76 | 也可以这样打断点,使用 **debugger** 关键字 77 | 78 | ```diff 79 | + debugger; 80 | const yang = 'yang' 81 | const yangyang = yang + 'yang' 82 | console.log(yangyang + 'yang'); 83 | ``` 84 | 85 | #### 3.点击运行和调试按钮 86 | 87 |  88 | 89 | #### 4.这里直接选择 Nodejs 环境 90 | 91 |  92 | 93 | 这时候你会发现,调试模式已经启动了,成功 ✅ 94 |  95 | 96 | #### 当然第 4 步这里其实还是要说明一下 97 | 98 | > 4.这里直接选择 Nodejs 环境 99 | > 你目前所在的工作文件是`debugger.js`,所以当你这里直接选择 debugger.js 文件之后,等同于 vscode 帮你`自动创建`了调试的`配置文件`,并把 **`program`** 字段指向了我们的 debugger.js 100 | 101 | 你可以使用配置文件尝试一下,效果是`等价`的 102 | 103 | #### 创建调试配置文件 104 | 105 |  106 | 107 | #### 配置文件的 program 指向谁,相当于启动调试哪个 Nodejs 文件 108 | 109 | ```diff 110 | // .vscode/launch.json 111 | +{ 112 | + // 使用 IntelliSense 了解相关属性。 113 | + // 悬停以查看现有属性的描述。 114 | + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 115 | + "version": "0.2.0", 116 | + "configurations": [ 117 | + { 118 | + "type": "node", 119 | + "request": "launch", 120 | + "name": "启动程序", 121 | + "skipFiles": [ 122 | + "/**" 123 | + ], 124 | + "program": "${workspaceFolder}/debugger.js" 125 | + } 126 | + ] 127 | +} 128 | ``` 129 | 130 | ## 问题来了 131 | 132 | 调试 Nodejs 文件我会了,但是命令行工具是启动脚本啊, `bin脚本` 该怎么调试呢? 133 | 134 | 我教给大家一种万能的方法,不管你的项目结构多复杂,有多少 packages,这样调试 bin 脚本,就非常简单 135 | 136 | #### 在我们的 debugger 文件夹下创建 bin 脚本 137 | 138 | ``` 139 | pnpm init 140 | ``` 141 | 142 | ```diff 143 | // package.json 144 | "bin": { 145 | + "debugger": "./bin/debugger.js" 146 | }, 147 | ``` 148 | 149 | ```ts 150 | // bin/debugger.js 151 | #!/usr/bin/env node 152 | const yang = 'yang' 153 | const yangyang = yang + 'yang' 154 | console.log(yangyang + 'yang'); 155 | ``` 156 | 157 | 验证 bin 脚本成功 158 | 159 | ``` 160 | npm link 161 | debugger 162 | // yangyangynag 163 | ``` 164 | 165 | #### 进入配置文件 166 | 167 | ```diff 168 | // .vscode/launch.json 169 | { 170 | // 使用 IntelliSense 了解相关属性。 171 | // 悬停以查看现有属性的描述。 172 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 173 | "version": "0.2.0", 174 | "configurations": [ 175 | { 176 | "type": "node", 177 | "request": "launch", 178 | "name": "启动程序", 179 | "skipFiles": [ 180 | "/**" 181 | ], 182 | - "program": "${workspaceFolder}/debugger.js" 183 | + "program": "${workspaceFolder}/bin/debugger.js" 184 | } 185 | ] 186 | } 187 | ``` 188 | 189 | 其实就是把 NodeJS 文件 的入口指定成我们的 bin 脚本 的入口,是不是有点过于简单了 190 | 191 | 因为 `npm run xxx` 的`本质`就是执行 `Node环境`的 bin 脚本 而 Node 环境的 bin 脚本 就是`执行Nodejs代码` 192 |  193 | 194 | ## 3.什么是 sourcemap?调试中为什么需要 sourcemap? 195 | 196 | 1.通常我们写好代码之后都会在`打包`的时候进行`压缩`、`polyfill`等一系列优化处理 197 | 198 | 2.为了让使用者得到良好的`类型支持`,现代大多数类库都会选择用 `TypeScript` 进行开发,ts 到 js 需要编译处理 199 | 200 | ### 什么是 SourceMap? 201 | 202 | `SourceMap` 是一个信息文件,里面存储了代码打包编译转换后的位置信息 203 | 通过开启 sourcemap 之后生成的 xxx.map 文件,你就可以知道 204 | 205 | - 打包之后的哪一个文件对应打包之前的哪一个文件 206 | 207 | 当然,它能清晰的对应到具体的`代码行数` 208 | 209 | > no say, let's code 210 | > 这里在前几小节已经详解过了,不过多赘述 211 | 212 | ``` 213 | tsc --init 214 | ``` 215 | 216 | ```diff 217 | // tsconfig.json 218 | + "declaration": true, //开启 ts声明文件 -- d.ts 219 | ``` 220 | 221 | ```ts 222 | // src/index.ts 223 | export const yang: string = 'yang' 224 | 225 | const yangyang = yang + 'yang' 226 | 227 | console.log(yangyang + 'yang') 228 | ``` 229 | 230 | ``` 231 | npm i father 232 | ``` 233 | 234 | ```diff 235 | // .fatherrc.ts 236 | import { defineConfig } from 'father' 237 | export default defineConfig({ 238 | cjs: { 239 | output: "dist", 240 | + sourcemap: true // 开启编译时sourcemap 241 | } 242 | }) 243 | ``` 244 | 245 | ```diff 246 | // package.json 247 | "scripts": { 248 | + "dev": "father dev", 249 | + "build": "father build" 250 | }, 251 | ``` 252 | 253 | ``` 254 | npm run dev 255 | ``` 256 | 257 | 成功生成 `产物` 和 `.map`文件 258 |  259 | 260 | 我们来分别看下生成的 `产物` 和 `sourcemap文件` 261 | 262 | ```js 263 | // 产物文件:dist/index.js 264 | var __defProp = Object.defineProperty 265 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor 266 | var __getOwnPropNames = Object.getOwnPropertyNames 267 | var __hasOwnProp = Object.prototype.hasOwnProperty 268 | var __export = (target, all) => { 269 | for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }) 270 | } 271 | var __copyProps = (to, from, except, desc) => { 272 | if ((from && typeof from === 'object') || typeof from === 'function') { 273 | for (let key of __getOwnPropNames(from)) 274 | if (!__hasOwnProp.call(to, key) && key !== except) 275 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }) 276 | } 277 | return to 278 | } 279 | var __toCommonJS = (mod) => __copyProps(__defProp({}, '__esModule', { value: true }), mod) 280 | 281 | // src/index.ts 282 | var src_exports = {} 283 | __export(src_exports, { 284 | yang: () => yang, 285 | }) 286 | module.exports = __toCommonJS(src_exports) 287 | var yang = 'yang' 288 | var yangyang = yang + 'yang' 289 | console.log(yangyang + 'yang') 290 | // Annotate the CommonJS export names for ESM import in node: 291 | 0 && 292 | (module.exports = { 293 | yang, 294 | }) 295 | //# sourceMappingURL=index.js.map 296 | ``` 297 | 298 | ```js 299 | // sourcemap文件:dist/index.js.map 300 | { 301 | "version": 3, 302 | "sources": ["../src/index.ts"], 303 | "sourcesContent": ["export const yang: string = 'yang'\n\nconst yangyang = yang + 'yang'\n\nconsole.log(yangyang + 'yang');\n"], 304 | "mappings": ";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAO,IAAM,OAAe;AAE5B,IAAM,WAAW,OAAO;AAExB,QAAQ,IAAI,WAAW,MAAM;", 305 | "names": [] 306 | } 307 | ``` 308 | 309 | 产物中指明了对应的 `sourcemap `文件 310 | 311 | > sourceMappingURL=index.js.map 312 | > sourcemap 文件中也指明了对应的`源产物`和`sourcesContent` 313 | 314 | ### 接下来,我们来利用 sourcemap`打断点调试` 315 | 316 | ```diff 317 | // bin/debugger.js 318 | #!/usr/bin/env node 319 | + require('../dist/index.js') 320 | - const yang = 'yang' 321 | - const yangyang = yang + 'yang' 322 | - console.log(yangyang + 'yang'); 323 | ``` 324 | 325 | 在 `src/index.ts` 里面打断点 326 | 327 |  328 | 调试配置文件的 **`program`** 字段 还是指向 `bin/debugger.js` 329 | 330 | ```json 331 | // .vscode/launch.json 332 | { 333 | // 使用 IntelliSense 了解相关属性。 334 | // 悬停以查看现有属性的描述。 335 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 336 | "version": "0.2.0", 337 | "configurations": [ 338 | { 339 | "type": "node", 340 | "request": "launch", 341 | "name": "启动程序", 342 | "skipFiles": ["/**"], 343 | "program": "${workspaceFolder}/bin/debugger.js" 344 | } 345 | ] 346 | } 347 | ``` 348 | 349 | 现在我们就可以 **`直接调试源代码`** 而不是`打包压缩编译`之后`看不懂`的代码了 350 | 351 | 他会在执行编译之后的产物代码的时候,自动定位到编译之前的代码,这样我们就可以调试 **`源码`** 了! 352 |  353 | 354 | ### 所以你可以回答我们为什么需要 sourcemap 吗? 355 | 356 | 如果你能看懂`编译`、`polyfill`、`压缩`等一系列操作之后之后生成的产物代码,就不需要 sourcmap 了(嘿嘿 357 | 358 | ## 4.实战:调试 **`Umi`** 源代码 359 | 360 | 我们来看 [Umi](https://github.com/umijs/umi) 的源码 361 | 362 | Umi 这个仓库采用 pnpm 的 `Monorepo` 管理 363 | 364 | 我们随便进入一个其中一个 package,可以发现也是使用 **`father`** 进行`编译` 365 | 366 | ### 1. clone [Umi](https://github.com/umijs/umi) 源码到本地 367 | 368 | ``` 369 | // 终端 370 | git clone https://github.com/umijs/umi.git 371 | code umi 372 | ``` 373 | 374 | ### 2.寻找 bin 脚本入口 375 | 376 | tips:可以在某个项目中安装 Umi,然后查看 `node_modules` 下 umi 中的 bin 命令 377 | 378 |  379 | 380 | 然后我们就可以在 Umi`源码` 中去定位入口 `bin` 脚本的位置了 381 | -- packages/umi/bin/umi.js 382 | 383 | ```js 384 | // packages/umi/bin/umi.js 385 | #!/usr/bin/env node 386 | 387 | // disable since it's conflicted with typescript cjs + dynamic import 388 | // require('v8-compile-cache'); 389 | 390 | // patch console for debug 391 | // ref: https://remysharp.com/2014/05/23/where-is-that-console-log 392 | if (process.env.DEBUG_CONSOLE) { 393 | ['log', 'warn', 'error'].forEach((method) => { 394 | const old = console[method]; 395 | console[method] = function () { 396 | let stack = new Error().stack.split(/\n/); 397 | // Chrome includes a single "Error" line, FF doesn't. 398 | if (stack[0].indexOf('Error') === 0) { 399 | stack = stack.slice(1); 400 | } 401 | const args = [].slice.apply(arguments).concat([stack[1].trim()]); 402 | return old.apply(console, args); 403 | }; 404 | }); 405 | } 406 | 407 | require('../dist/cli/cli') 408 | .run() 409 | .catch((e) => { 410 | console.error(e); 411 | process.exit(1); 412 | }); 413 | ``` 414 | 415 | ### 3.安装依赖、生成 bin 脚本需要的 dist 目录 416 | 417 | ``` 418 | // 根目录 419 | pnpm i 420 | npm run build //执行了 turo run build 421 | ``` 422 | 423 | ### 4.设置调试配置 424 | 425 | 注意:这里新增一个 “stopOnEntry”: true,作用是会在入口文件第一行打断点,不需要我们亲手去打,可以从入口开始调试 426 | 427 | ```json 428 | // .vscode/launch.json 429 | { 430 | // 使用 IntelliSense 了解相关属性。 431 | // 悬停以查看现有属性的描述。 432 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 433 | "version": "0.2.0", 434 | "configurations": [ 435 | { 436 | "type": "node", 437 | "request": "launch", 438 | "name": "启动程序", 439 | "program": "${workspaceFolder}/packages/umi/bin/umi.js", 440 | "args": ["dev"], 441 | "stopOnEntry": true 442 | } 443 | ] 444 | } 445 | ``` 446 | 447 | ### 5.出现问题 448 | 449 | 这里我们忘记了一个关键因素,sourcemap 450 | 我这里准备去他的 `.fatherrc.ts 配置文件`开启一下 sourcemap 451 | 452 | 结果发现他现在 father 版本是`3.x`,`不支持`开启`sourcemap` 453 | 454 | ## 5.基于调试中的问题,给 Umi 提个 pr 455 | 456 | 先提个`issue`:https://github.com/umijs/umi/issues/9876 457 | `pr`--升级 father 版本到 4.x 支持`sourcemap`:https://github.com/umijs/umi/pull/9877 458 | 459 | 到现在为止,该 pr 已经`合并` 460 | 461 | ### 这样,我们就可以开始调试 Umi `源代码`了!而且每次都会进入源码 ts 文件中 ✅ 462 | -------------------------------------------------------------------------------- /手写可插拔前端框架小册/1-4.仓库架构-Monorepo仓库实践.md: -------------------------------------------------------------------------------- 1 | ### 该小节主要知识点: 2 | 3 | - 什么是 `Monorepo` ? 4 | - ⭐️⭐️⭐️⭐ 搭建 mini-umi 项目并`实践` `Monorepo` 组织管理 5 | - Monorepo 中的 `构建顺序`问题 与 `循环依赖` 6 | - `Turborepo` `高性能`构建系统 接入与`开源项目`实践 7 | 8 | > 本小节所有代码已放入 [mini-umi](https://github.com/BoyYangzai/mini-umi) 仓库 `/juejin/mini-umi`目录 下 9 | 10 | # 什么是 `Monorepo` ? 11 | 12 | Monorepo 是一种组织管理代码的方式,指在`一个 git 仓库`中管理`多个项目` 13 | 项目结构一般为: 14 | 15 | ```js 16 | . 17 | ├── package.json 18 | └── packages 19 | ├── packageA 20 | ├── packageB 21 | └── packageC 22 | ``` 23 | 24 | ### 这样管理仓库有什么 `好处` 呢? 25 | 26 | - Monorepo 可以通过 `workspace` 快速`软链接`link,方便 npm 包 的`本地开发` 27 | - 可以使用`统一的工程化配置` 如 tsconfig eslint prettier 等 28 | - 抽取`公共依赖` 减少重复安装 29 | 30 | # 搭建 mini-umi 项目并实践 `Monorepo` 组织管理 31 | 32 | ## 1.根目录搭建 33 | 34 | ### 新建项目目录 35 | 36 | ``` 37 | mkdir mini-umi 38 | code mini-umi 39 | ``` 40 | 41 | ### 搭建项目骨架 42 | 43 | ```js 44 | pnpm init // 新建 package.json 45 | pnpm i typescript -d // 安装 typescript 46 | tsc --init // 新建 tsconfig 文件 47 | ``` 48 | 49 | ### 修改 tsconfig.json 50 | 51 | ```json 52 | { 53 | "compilerOptions": { 54 | "strict": true, 55 | "declaration": true, 56 | "skipLibCheck": true, 57 | "baseUrl": "./", 58 | "moduleDetection": "auto", 59 | "esModuleInterop": true 60 | } 61 | } 62 | ``` 63 | 64 | ### 使用 `father` 代替 `tsc` 65 | 66 | ### 什么是 **father** ? 67 | 68 | father 是 `蚂蚁体验技术部`推出的一款 `NPM 包研发工具`,能够帮助开发者更高效、高质量地研发 NPM 包、生成构建产物、再完成发布。它主要具备以下特性: 69 | 70 | > - ⚔️ 双模式构建: 支持 Bundless 及 Bundle 两种构建模式,ESModule 及 CommonJS 产物使用 Bundless 模式,UMD 产物使用 Bundle 模式 71 | > - 🎛 多构建核心: Bundle 模式使用 Webpack 作为构建核心,Bundless 模式支持 esbuild、Babel 及 SWC 三种构建核心,可通过配置自由切换 72 | > - 🔖 类型生成: 无论是源码构建还是依赖预打包,都支持为 TypeScript 模块生成 .d.ts 类型定义 73 | > - 🚀 持久缓存: 所有产物类型均支持持久缓存,二次构建或增量构建只需『嗖』的一下 74 | > - 🩺 项目体检: 对 NPM 包研发常见误区做检查,让每一次发布都更加稳健 75 | > - 🏗 微生成器: 为项目追加生成常见的工程化能力,例如使用 jest 编写测试 76 | > - 📦 依赖预打包: 开箱即用的依赖预打包能力,帮助 Node.js 框架/库提升稳定性、不受上游依赖更新影响(实验性) 77 | > 以上摘自 father-#README.md 78 | 79 | ### 为什么要使用 father? 80 | 81 | 因为他内置了 `Bundless` 和 `Bundle` 两种构建模式,而且内置有很方便的脚手架 82 | 83 | tips:father 也是`基于Umi微内核架构`开发的 84 | 85 | ### 这里我们`不使用` father 内置脚手架 86 | 87 | ``` 88 | npm i father 89 | ``` 90 | 91 | ### 新建 .fatherrc.ts 配置文件 92 | 93 | ```ts 94 | import { defineConfig } from 'father' 95 | 96 | export default defineConfig({ 97 | cjs: { 98 | output: 'dist', 99 | sourcemap: true, 100 | }, 101 | }) 102 | ``` 103 | 104 | ### 新建 src/index.ts 105 | 106 | ```ts 107 | export const yang = 'yang' 108 | console.log(yang) 109 | ``` 110 | 111 | ### 修改 package.json 112 | 113 | ```json 114 | { 115 | "name": "mini-umi", 116 | "version": "1.0.0", 117 | "description": "a simple model for Umi", 118 | "main": "./dist/index.js", 119 | "types": "./dist/index.d.ts", 120 | "scripts": { 121 | "dev": "father dev" // 与 tsc --watch 类似 122 | }, 123 | "keywords": [], 124 | "author": "洋", 125 | "license": "ISC", 126 | "dependencies": { 127 | "father": "^4.1.0" 128 | } 129 | } 130 | ``` 131 | 132 | ### 运行 dev 脚本 133 | 134 | ``` 135 | npm run dev 136 | ``` 137 | 138 | 效果为下图即算成功 139 | 140 |  141 | 142 | ## 2.创建 `workspace` 工作区 143 | 144 | 在工作区内的 package 可以很方便的使用`软链接引用` 145 | 146 | ```yaml 147 | # pnpm-workspace.yaml(根目录) 148 | packages: 149 | - 'packages/*' 150 | ``` 151 | 152 | ## 3.子包搭建 153 | 154 | 删除 src,新建`packages`目录 在 package 目录下 创建如下结构: 155 | 156 | ```js 157 | . 158 | ├── package.json 159 | └── packages 160 | ├── packageA 161 | ├── packageB 162 | └── packageC 163 | ``` 164 | 165 | 这里与根目录搭建基本一致 大家自己试着创建一下 166 | 167 | 168 | 最终效果如下: 169 | 170 |  171 | 172 | #### 问题 1:根目录中的 .fatherrc.ts 为什么要改名为 .fatherrc.base.ts (内容不变) 173 | 174 | 因为我们这边`子目录`中的 `.fatherrc.ts` 配置文件要`继承根目录`中的配置 175 | 176 | ```ts 177 | import { defineConfig } from 'father' 178 | 179 | export default defineConfig({ 180 | extends: '../../.fatherrc.base.ts', 181 | }) 182 | ``` 183 | 184 | #### 问题 2:为什么 tsconfig 不需要在每个子项目里面再写一份呢? 185 | 186 | 还记得 Monorepo 有个`优点`之一 `可以使用 统一的工程化配置`, 像 tsconfig 我们只需要在根目录写一份即可在所有包里生效 187 | 188 | ## 4.使用 workspace 189 | 190 | ### 1.在所有子项目 package.json 中加入构建脚本 191 | 192 | ```diff 193 | "scripts": { 194 | "dev": "father dev", 195 | + "build": "father build" 196 | }, 197 | ``` 198 | 199 | ### 2.在根目录 package.json 中加入 200 | 201 | ```diff 202 | "scripts": { 203 | "dev": "father dev", 204 | + "build:all": "pnpm run -r build" 205 | }, 206 | ``` 207 | 208 | ### 3.在根目录运行脚本 209 | 210 | ``` 211 | npm run build:all 212 | ``` 213 | 214 | 效果如下,所有子包全部打包产物 dist 目录成功,说明 workspace 设置成功 215 |  216 | 217 | ### 原理:pnpm run -r xxx 218 | 219 | `-r` 指的是`递归` 所以这里会递归执行所有`workspace`里的 xxx 命令,上述示例中即为 build 命令 220 | 221 | ### ⭐️⭐️⭐️⭐ `核心:`4.使用软链接 222 | 223 | ##### 1.修改 A 包 配置 224 | 225 | ```diff 226 | //package/packageA/package.json 227 | "dependencies": { 228 | + "package-b": "workspace:*" 229 | } 230 | ``` 231 | 232 | 这里修改完毕依赖之后一定要重新安装依赖 `pnpm i` 233 | 出现链接标记即算成功 点开观察发现 `package-b` 就是我们旁边的 `B包` 234 | 235 |  236 | 237 | ##### 2.修改 A 包 内容 238 | 239 | ```ts 240 | // package/packageA/src/index.ts 241 | import { yang } from 'package-b' 242 | console.log(yang) 243 | ``` 244 | 245 | ##### 3.修改 B 包 内容 246 | 247 | ```ts 248 | export const yang = 'yang from b' 249 | ``` 250 | 251 | ##### 4.也别忘了修改 B 包 `导出的入口文件` 252 | 253 | ```diff 254 | // package/packageB/package.json 255 | - "main": "index.js", 256 | + "main": "./dist/index.js", 257 | ``` 258 | 259 | ##### 5.在根目录打包`所有子包产物` 260 | 261 | ``` 262 | npm run build:all 263 | ``` 264 | 265 | ##### 6.在 A 包中 266 | 267 | ``` 268 | node dist/index.js 269 | ``` 270 | 271 |  272 | 273 | 到这里,我们 mini-umi 的 Monorepo 仓库就算是基本`搭建成功`了,是不是很简单(〃'▽'〃) 274 | 275 | 先接着往下看 276 | 277 | # Monorepo 中的 `构建顺序`问题 与 `循环依赖` 278 | 279 | ### 上述步骤中的 2-4-5 其实是很方便的 280 | 281 | > 5.在根目录打包`所有子包产物` 282 | > npm run build:all 283 | 284 | ## 问题一:`构建顺序`问题: 285 | 286 | 前面提到了 npm run build:all 时 其实执行了 `pnpm run -r build` 287 | 288 | 它会去自动分析依赖关系,得到`递归的顺序 ` 289 | 你会发现 它先执行了 B 包的 build 再去执行 A 包的 Build 290 | 这明显`符合我们的预期` 291 | 292 | 因为我们的 `A包 依赖于 B包的产物`,所以正确顺序应该是 `先build出 B包的产物`,`再build A包` 293 | 294 | 但是,要是不使用 `pnpm run -r build`,该怎么解决构建顺序的问题呢? 295 | 296 | ### 如何解决`构建顺序`问题呢? 297 | 298 | #### 1. 手动 先 build A 包 再 build B 包 299 | 300 | 优势:可控 301 | 劣势:管理的子包一旦庞大,可能要`手动 build 几十次` 302 | 303 | #### 2. 写不同的脚本组合 304 | 305 | 这个方案就比较多了,你可以写各种各样的脚本组合在一起去保证他的构建顺序 306 | 优势:方案较多 307 | 劣势:要写脚本 我懒 308 | 309 | #### 3. 使用可分析的构建--`Turborepo` 310 | 311 | 当我们使用 Turborepo 后他会去`自动分析`包的`引用关系`(这与`pnpm run -r build`是一致的),从而得到`正确的构建顺序` 312 | 下一小节将为我们的项目引入 Turborepo 313 | 314 | ## 问题二:`循环依赖`问题 315 | 316 | 循环依赖的例子很简单: 317 | A 包依赖了 B 包 B 包依赖了 A 包 318 | 这个问题乍一看很不正常 傻子才会这么用吧 但是确实有可能出现 比方说在`引用 ts 类型` 的时候 319 | 其实 pnpm workspace 是可以`支持循环依赖`的 320 | 问题在于 `Trubo 不支持循环依赖` 321 | 322 |  323 | 324 | 所以我们在 Monorepo 仓库中应尽量`避免`出现 `循环依赖` 的问题 325 | 326 | # Turborepo - `高性能`构建系统 接入与实践 327 | 328 | ## 什么是 `Turborepo` ? 329 | 330 | > Turborepo is a `high-performance` build system for JavaScript and TypeScript codebases. 331 | > **Turborepo** 是一个用于 **JavaScript** 和 **TypeScript** 代码库的`“高性能”`构建系统。 332 | > 通俗一点讲,他是我们在 **Monorepo** 仓库中的一种`优化构建`的`方案` 333 | > 这一点一定要区分于 Mutirepo 和 Monorepo 的 **`代码组织方案`** 334 | 335 | ## 使用 Turborepo 有什么好处呢? 336 | 337 | #### 官方的表述是: 338 | 339 | > Turborepo leverages advanced build system techniques to speed up development, **both on your local machine and your CI/CD**. 340 | > 341 | > [1.Never do the same work twice](https://turbo.build/repo/docs/core-concepts/caching) 342 | > 343 | > [Turborepo remembers the output of any task you run - and can skip work that's already been done.](https://turbo.build/repo/docs/core-concepts/caching) 344 | > 345 | > [2.Maximum Multitasking](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks) 346 | > 347 | > [The way you run your tasks is probably not optimized. Turborepo speeds them up with smart scheduling, minimising idle CPU's.](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks) 348 | 349 | #### 翻译过来就是: 350 | 351 | 1.构建缓存 -- 而且可以`远程缓存`,这在团队开发十分有用 2.智能调度`构建加速`,减少空闲 CPU 3.通过 `Pipeline` 定义任务之间的关系,加快构建速度 4.方便快捷的`配置文件` 352 | 当然,和 Monorepo 一样,当你去执行 `npm run build:all` 这种构建脚本时,Turborepo 会为你自动根据它们的依赖关系,达到最优的构建顺序 353 | 354 | ## 如何为一个项目接入 Turborepo? 355 | 356 | 其实这部分还是比较简单的,大致分为以下几步: 357 | 358 | ##### 1.确定你的 Monorepo 项目中的 workspace 正常 359 | 360 | 因为接下来 Turborepo 构建是要基于我们的 workspace 361 | 362 | ##### 2.安装 Turbo 363 | 364 | ``` 365 | pnpm add turbo -Dw 366 | ``` 367 | 368 | ##### 3.配置 Turbo 的配置文件 369 | 370 | ```json 371 | //turbo.json 这里是一套完整的官方示例 372 | { 373 | "$schema": "https://turbo.build/schema.json", 374 | "pipeline": { 375 | // 这是上面提到的任务管道 376 | "build": { 377 | // A package's `build` script depends on that package's 378 | // dependencies and devDependencies 379 | // `build` tasks being completed first 380 | // (the `^` symbol signifies `upstream`). 381 | "dependsOn": ["^build"], 382 | // note: output globs are relative to each package's `package.json` 383 | // (and not the monorepo root) 384 | "outputs": [".next/**"] 385 | }, 386 | "test": { 387 | // A package's `test` script depends on that package's 388 | // own `build` script being completed first. 389 | "dependsOn": ["build"], 390 | "outputs": [], 391 | // A package's `test` script should only be rerun when 392 | // either a `.tsx` or `.ts` file has changed in `src` or `test` folders. 393 | "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts", "test/**/*.tsx"] 394 | }, 395 | "lint": { 396 | // A package's `lint` script has no dependencies and 397 | // can be run whenever. It also has no filesystem outputs. 398 | "outputs": [] 399 | }, 400 | "deploy": { 401 | // A package's `deploy` script depends on the `build`, 402 | // `test`, and `lint` scripts of the same package 403 | // being completed. It also has no filesystem outputs. 404 | "dependsOn": ["build", "test", "lint"], 405 | "outputs": [] 406 | } 407 | } 408 | } 409 | ``` 410 | 411 | ##### 4.添加.gitignore 412 | 413 | 将它的`缓存文件` ignore 掉,防止上传到 Github 仓库 414 | 415 | ```diff 416 | // gitignore 417 | + .turbo 418 | ``` 419 | 420 | ##### 5.替换构建脚本 421 | 422 | ```diff 423 | - ”build“: "xxxx" 424 | + "build": "turbo run build" 425 | ``` 426 | 427 | ##### 6.(可选)远程构建缓存 428 | 429 | ``` 430 | npx turbo login // 登录远程缓存账号 431 | npx turbo link // 将当前构建仓库与远端 link 432 | ``` 433 | 434 | `远程构建缓存` 在`团队协同开发`的时候能提升不少的速度 435 | 436 | 到这里,我们的 Turborepo 接入基本上就完成了,只需要执行 `npm run build` 即可看到效果 437 | 438 | 下面是 `开源项目` 实战 439 | 440 | ## Turborepo 实战案例 441 | 442 | 这边我用`蚂蚁AntV团队`新开源的 [GraphInsight](https://github.com/antvis/GraphInsight) 为例接入 **Turborepo** 443 | 444 | 这是他现在的 `package.json` 445 | 446 | ```json 447 | "scripts": { 448 | "preinstall": "npx only-allow pnpm", 449 | "postinstall": "npm run build:all:es", 450 | "build:all:es": "pnpm run -r build:es", 451 | "start": "cd packages/gi-site && npm run start", 452 | "gi-common-component": "cd packages/gi-common-components && npm run build:es", 453 | "gi-sdk": "cd packages/gi-sdk && npm run build:es", 454 | "gi-assets-basic": "cd packages/gi-assets-basic && npm run build:es", 455 | "gi-assets-advance": "cd packages/gi-assets-advance && npm run build:es", 456 | "gi-assets-algorithm": "cd packages/gi-assets-algorithm && npm run build:es", 457 | "gi-assets-scene": "cd packages/gi-assets-scene && npm run build:es", 458 | "gi-assets-graphscope": "cd packages/gi-assets-graphscope && npm run build:es", 459 | "gi-assets-neo4j": "cd packages/gi-assets-neo4j && npm run build:es", 460 | "gi-assets-tugraph": "cd packages/gi-assets-tugraph && npm run build:es", 461 | "gi-theme-antd": "cd packages/gi-theme-antd && npm run build:es", 462 | "build": "npm run build:site && npm run move:dist", 463 | "build:assets": "cd packages/gi-assets-basic && NODE_OPTIONS=--max_old_space_size=2048 npm run build", 464 | "build:basicAssets": "cd packages/gi-assets-basic && NODE_OPTIONS=--max_old_space_size=2048 npm run build", 465 | "build:core": "cd packages/gi && NODE_OPTIONS=--max_old_space_size=2048 npm run build", 466 | "build:site": "cd packages/gi-site && pnpm install && NODE_OPTIONS=--max_old_space_size=2048 npm run build", 467 | "build:testing": "cd packages/gi-assets-testing && NODE_OPTIONS=--max_old_space_size=2048 npm run build", 468 | "clean:all": "pnpm run -r clean", 469 | "core": "cd packages/gi && npm run start", 470 | "move:dist": "node ./scripts/deploy.js", 471 | "site": "cd packages/gi-site && NODE_OPTIONS=--max_old_space_size=2048 npm run start" 472 | }, 473 | ``` 474 | 475 | 之前是 476 | 477 |  478 | 479 | 这里的 `build:all:es` 为了`保障执行顺序`,属实是 **'辛苦'** 它了,所以我给他提了 pr,将他用 `pnpm run -r build:es` 替换掉了 480 | 481 | #### 现在我们使用 `Turborepo` 替换掉它 482 | 483 | ##### 1.安装 Turbo 484 | 485 | ``` 486 | pnpm add turbo -Dw 487 | ``` 488 | 489 | ##### 2.加上配置文件 Pipeline 490 | 491 | ```diff 492 | // turbo.json 493 | +{ 494 | + "$schema": "https://turbo.build/schema.json", 495 | + "pipeline": { 496 | + "build:es": { 497 | + "dependsOn": [ 498 | + "^build:es" 499 | + ], 500 | + "outputs": [ 501 | + "es/**", 502 | + "lib/**" 503 | + ] 504 | + } 505 | + }, 506 | + "globalDependencies": [ 507 | + ".prettierrc.js" 508 | + ] 509 | +} 510 | ``` 511 | 512 | ##### 3.替换构建脚本 513 | 514 | ```diff 515 | // package.json 516 | - "build:all:es": "pnpm run -r build:es", 517 | + "build:all:es": "turbo run build:es", 518 | ``` 519 | 520 | ##### 4.缓存文件加入 gitignore 521 | 522 | ```diff 523 | // gitignore 524 | + .turbo 525 | ``` 526 | 527 | 执行 `"npm run build:all:es"` 即可看到效果: 528 | 529 | ##### ⭐️ 替换之前的 "pnpm run -r build:es" 530 | 531 | 共计用时:5.1+9.5+10.2+28.2+20.4+26 s 532 |  533 | 534 | ##### ⭐️ 替换之后的 "turo run build:es" 第一次构建+缓存 535 | 536 | 共计用时 44 s 537 |  538 | 539 | ##### ⭐️ 缓存之后的 "turo run build:es" 构建 540 | 541 | 平均时间: **`800+ms`** 542 |  543 | 544 |  545 | 546 |  547 | 548 | 从 `40s+` 优化到了 `1s` 以下,只能说非常香了 549 | 550 | 直接给 AntV 提交 [Pr](https://github.com/antvis/GraphInsight/pull/66) 551 | 552 |  553 | 554 | ## 小结: 555 | 556 | **本小节我们** 557 | 558 | **1.首先为大家介绍了什么是 Monorepo。它其实是一种代码仓库的组织管理方式,通过 Monorepo 的管理,我们可以很`方便`的在 workspace 中开发本地 npm 库,`统一规范配置`等** 559 | 560 | **2.通过代码带着大家搭建好了一个实用的 `Monorepo仓库`,防止大家踩坑,并学会了一些 Monorepo 开发中的`小技巧`,也引出了我们下文中的 `pnpm构建顺序问题` 以及 `Turborepo`** 561 | 562 | **3.为大家介绍了 pnpm 使用 Monorepo 管理仓库出现的两个问题:`构建顺序`以及`循环依赖`问题以及它们的解决方案** 563 | 564 | **4.引入了现代高性能构建方案-`Turborepo`,简述了`如何`在一个 Monorepo 项目中接入 Turbo,并以蚂蚁体验技术部开源产品 `GraphInsight` 为了进行实战接入演示效果** 565 | 566 | **下一小节** 567 | 568 | ## 小节思考: 569 | 570 | **1.Monorepo 管理仓库 究竟有哪些好处呢?它与`传统 Mutirepo仓库` 有哪些优劣你能说的上来吗** 571 | **2.你能在我们的新建的 mini-umi 项目中接入 Turborepo 吗,试一下吧** 572 | **3.你知道新建 `.fatherrc.ts` 配置文件的时候 为什么 export default 后面要使用一个`defineConfig 函数`吗** 573 | 2.1 已知信息一:defineConfig 函数 参数是 `config` 返回值也是`config` 574 | 2.2 已知信息二:`vite 配置文件`中也是同样的用法 575 | 576 | ```ts 577 | import { defineConfig } from 'father' 578 | export default defineConfig({ 579 | cjs: { 580 | output: 'dist', 581 | sourcemap: true, 582 | }, 583 | }) 584 | ``` 585 | --------------------------------------------------------------------------------