背景
之前不是说了吗,公司预备升到 Vue3。后面又因为一些需求把项目结构改成了 qiankun 和 Monorepo,所以这篇文章便是一个建设模板的讲解。
老话说的好,打 BOSS 前要把 Buff 加满,不是,是工欲善其事,必先利其器。在其它小伙伴过来写代码之前需求把重复工作的模板搭好,开发效率才能更高。
所以咱们的方针便是,ESLint
、Prettier
、TypeScript
之类的东西装备好,能够用指令行发动整个项目,能够用指令行新建微运用。能够让其他人零装备
直接上手写代码。
开端
本项目默许包办理器运用 pnpm,IDE 运用 VSCode
pnpm create vite
TypeScript
看下依靠,只要 vue-tsc
不熟,查下文档。(留意下图红框)
看起来是一个查看 Vue SFC
文件 TS 类型的指令行东西,比方在打包或许在 merge 代码前查看 TS 类型是否正确,而在 dev 环境下运用 Volar 插件来验证。
这样看来,需求引荐装置 Volar 插件。
在根目录新建 .vscode
目录,增加
{
"recommendations": ["johnsoncodehk.volar", "johnsoncodehk.vscode-typescript-vue-plugin"]
}
这样其他用 VSCode 翻开这个项目的人就会弹出引荐装置该插件。
插件的姓名能够在概况查看
这样开发时就有 TS 类型约束了。
并且装置这两个插件之后,也不需求本来的 *.vue
的类型声明晰。
ESLint
pnpm install eslint eslint-plugin-vue @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
-
ESLint
JavaScript 代码查看东西
-
eslint-plugin-vue
Vue 官方的 ESLint 插件,包含 vue/* 的规矩集
-
@typescript-eslint/eslint-plugin
TS 查看的 ESLint 插件,包含 @typescript-eslint/* 的规矩集
-
@typescript-eslint/parser
协助 ESLint 解析 TypeScript 语法
需求留意的
@typescript-eslint/parser
和@typescript-eslint/eslint-plugin
版本号必需共同
增加 .eslintrc.js
,.eslintignore
文件
pnpm install eslint-define-config -D
.eslintrc.js
// @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
root: true,
rules: {
// rules...
},
});
这样 eslintrc
也有类型约束了
同样,VSCode 的 ESLint 插件也增加到引荐 JSON 里。下面不在提示了。
Prettier
Volar
、ESLint
都有格式化功用,那为什么还要选择 Prettier
呢?
代码风格最不好办理的当地在哪里? 便是所有人都有自己的习惯,无法评论出一个让所有人都满意的计划。
Prettier 的计划是 opinionated
,便是给出最小的装备,要不你就用我的风格,要不你就别用。不容许随意改。(所以也不必吵了。。。)
留意
代码风格
和语法查看
的区别,代码风格是要不要加空格,分号,逗号等等。语法查看是是否声明变量,声明未运用等等。
pnpm install -D eslint-config-prettier eslint-plugin-prettier prettier
-
eslint-config-prettier
封闭和 Prettier 抵触的 ESLint 规矩
-
eslint-plugin-prettier
把 Prettier 规矩嵌入到 ESLint,下面代码的 prettier/prettier": "error"
修正.eslintrc.js
装备
{
"extends": [
"some-other-config-you-use",
"prettier"
],
plugins: ["prettier"],
rules: {
"other-rules",
"prettier/prettier": "error"
}
}
在 VSCode 装置 Prettier 插件,把格式化方式改为 Prettire
,测验格式化正常。
Stylelint
pnpm install --D stylelint stylelint-config-recommended-vue stylelint-config-prettier stylelint-config-recess-order postcss-html
-
Stylelint
强壮、先进的 CSS 代码查看器,能够查看 CSS 代码中的错误和风格
-
stylelint-config-recommended-vue
Vue 的 Stylelint 的引荐规矩集
-
stylelint-config-prettier
封闭和 Prettier 抵触的规矩
-
stylelint-config-recess-order
对 CSS 属性进行排序
-
postcss-html
解析 HTML 或许类 HTML 的 PostCSS 语法,比方 PHP、 VueSFC
新建.stylelintrc.json
{
"extends": ["stylelint-config-recommended-vue", "stylelint-config-prettier", "stylelint-config-recess-order"]
}
需求留意几个问题
- 需求封闭 VSCode 自己的 css 验证,否则会有两个验证报错 settings.json
"css.validate": false,
"less.validate": false,
"scss.validate": false
- 假如
.stylelintrc
装备文件是用 .json 结束的,不要加注释。不是 JSON5,会导致拉取装备无法编译。 -
stylelint-config-standard
默许规矩只对 css 生效,无法解析 Vue - 假如运用预编译器,需求替换对应的规矩集
比方要运用
scss
pnpm install stylelint-config-standard-scss -D
.stylelintrc.json
装备改为
{
"extends": [
"stylelint-config-standard-scss",
"stylelint-config-recommended-vue/scss",
"stylelint-config-prettier",
"stylelint-config-recess-order"
]
}
Windi CSS
pnpm i -D vite-plugin-windicss windicss
然后,在你的 Vite 装备中增加插件:
vite.config.js
import WindiCSS from 'vite-plugin-windicss'
export default {
plugins: [
WindiCSS(),
],
}
最终,在你的 Vite 入口文件中导入virtual:windi.css
:
main.js
import 'virtual:windi.css'
装置 VSCode 插件,在 settings.json
里增加
"editor.quickSuggestions": {
"strings": true
},
这样就会有代码提示
和悬停预览
等等功用
在 windi.config.ts
中装备
import { defineConfig } from 'windicss/helpers'
export default defineConfig({
extract: {
include: [
'src/**/*.{vue,jsx,tsx,svelte}',
'shared/**/*.{vue,ts}',
],
},
})
然后,在 VSCode 中 Ctrl+Shift+P
, 运转指令:Windi CSS: Run & Open Analysis
能够翻开 windicss
的可视化分析。
生产依靠
演示就用 antd 了
pnpm install @ant-design/icons-vue ant-design-vue @vueuse/core pinia vue-router
业务相关代码,比方布局啊,大局主题啊不是这次的要害,就不详细写了。
monorepo
pnpm 运用 monorepo 十分简略,只需求加个文件 pnpm-workspace.yaml
,然后新建一个packages
文件夹
prefer-workspace-packages: true
packages:
- 'packages/**'
这样 packages
里便是独自的 repo
,而各种装备和依靠能够共用大局的。
qiankun
为了让同享装备层级清晰,qiankun 主运用(基座)和子运用都放到 packages
中。
pnpm install -W qiankun
大局依靠需求加 -W
主运用几个需求改到的当地。
- 路由,需求把子运用注册路由加进来
// 获取微服务路由
const microRoutes = []
microApps.forEach(micro => {
microRoutes.push({
path: `${micro.activeRule}/:morePath*`,
component: Layout
})
})
// 创立路由实例
export const router: Router = createRouter({
history: createWebHistory(),
routes: microRoutes.concat([
{
path: "/",
component: Layout,
redirect: "/app1"
}
])
})
- 在
main.ts
入口处,增加注册信息microApps.ts
type microApp = {
name: string
entry: string
container: "#view-main"
activeRule: string
port: number | string
}
const apps: microApp[] = [
{
name: "app1",
entry: "//localhost:10002",
container: "#view-main",
activeRule: "/app1",
port: "10002"
}
]
export default apps
import { start as startQianKun, registerMicroApps, initGlobalState, MicroAppStateActions } from "qiankun"
// 微运用的信息
import apps from "./microApps"
/**
* 注册微运用
* 第一个参数 - 微运用的注册信息
* 第二个参数 - 大局生命周期钩子
*/
registerMicroApps(apps, {
// qiankun 生命周期钩子 - 微运用加载前
beforeLoad: (app: any) => {
// 加载微运用前,加载进度条
// eslint-disable-next-line no-console
console.log("before load app.name====>>>>>", app.name)
return Promise.resolve(app)
},
beforeMount: (app: any) => {
// eslint-disable-next-line no-console
console.log("[LifeCycle] before mount %c%s", "color: green;", app.name)
return Promise.resolve(app)
},
// qiankun 生命周期钩子 - 微运用挂载后
afterMount: (app: any) => {
// 加载微运用前,进度条加载完结
// eslint-disable-next-line no-console
console.log("[LifeCycle] after mount %c%s", "color: green;", app.name)
return Promise.resolve(app)
}
})
/**
* qiankun 通讯实例
*/
const initialState = {}
export const actions: MicroAppStateActions = initGlobalState(initialState)
// 注册 qiankun
startQianKun({ sandbox: { strictStyleIsolation: true } })
// qiankun 通讯
actions.onGlobalStateChange((state, prevState) => {
// state: 改变后的状态; prevState: 改变前的状态
// console.log(state, prevState)
})
- 布局处增加子运用挂载的入口
<a-layout>
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
<div class="logo" />
<Menu></Menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="padding: 0; background: #fff">
<menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
<menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
</a-layout-header>
<a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }">
<!-- 子运用容器 -->
<div id="view-main" />
<!-- 主运用自己的路由烘托 -->
<router-view></router-view>
</a-layout-content>
</a-layout>
</a-layout>
子运用需求留意的位置
-
package.json
中name
需求和主运用注册的name
共同 -
vite.config.ts
里server.port
需求改为主运用注册的entry
端口共同 - 子运用路由
export const router: Router = createRouter({
history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? `/${packageConfig.name}` : "/"),
})
-
vite.config.ts
增加插件
import packageConfig from "./package.json"
import qiankun from "vite-plugin-qiankun"
const useDevMode = true // 假如是在主运用中加载子运用 vite, 有必要翻开这个, 否则vite加载不成功, 独自运转没影响
plugins: [
qiankun(`${packageConfig.name}`, { useDevMode })
]
-
main.ts
改为
import { createApp } from "vue"
import { createPinia } from "pinia"
import router from "./router"
import { renderWithQiankun, qiankunWindow, QiankunProps } from "vite-plugin-qiankun/dist/helper"
import { MicroAppStateActions } from "qiankun"
import Antd from "ant-design-vue"
import "ant-design-vue/dist/antd.variable.min.css"
import "virtual:windi.css" // windi Css
import App from "./App.vue"
let instance = null
type AppProps = Partial<MicroAppStateActions & QiankunProps>
function render(props: AppProps) {
const { container } = props
if (container) {
// 注册主,微运用大局通讯
props.onGlobalStateChange((state, prevState) => {
// useTheme().theme({ state, prevState, container })
})
}
instance = createApp(App)
instance.use(router).use(createPinia).use(Antd)
instance.mount(container ? container.querySelector("#app") : document.getElementById("app"))
if (qiankunWindow.__POWERED_BY_QIANKUN__) {
// console.log('我正在作为子运用运转')
}
}
renderWithQiankun({
/**
* 运用每次进入都会调用 mount 办法,一般咱们在这里触发运用的烘托办法
* @param props
*/
mount(props: MicroAppStateActions & QiankunProps) {
render(props)
},
/**
* bootstrap 只会在微运用初始化的时分调用一次,下次微运用从头进入时会直接调用 mount 钩子
*/
bootstrap() {
// 一般咱们能够在这里做一些大局变量的初始化,比方不会在 unmount 阶段被销毁的运用级别的缓存等。
},
/**
* 运用每次切出/卸载 会调用的办法,一般在这里咱们会卸载微运用的运用实例
*/
unmount() {
instance.unmount()
instance._container.innerHTML = ""
instance = null
}
})
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({})
}
在大局 package.json
增加发动指令
"scripts": {
"dev": "pnpm -r --filter ./packages --parallel run dev",
"build": "pnpm -r --filter ./packages --parallel run build",
"preview": "pnpm -r --filter ./packages --parallel run preview",
"clean": "rm -rf node_modules **/*/node_modules"
}
这样,整个项目就布置好了。
整合装备
但是,现在所有的装备仍是分散的,需求整合下。
ESLint
、Prettierr
和 Stylelint
用大局的一份装备就行。
packages.json
主子运用都有依靠放到大局,子运用独自用的放到自己文件内。
- 主运用大局依靠
pnpm install xxxx -W
其他指令和原指令共同,多增加一个-W
- 子运用依靠装置
pnpm i xxxx --filter ${微运用pwd}
比方pnpm uninstall uuid --filter app1
会在app1
子运用下装置uuid
依靠,当然也能够去子运用目录下直接装置
tsconfig.json
运用 extends
承继大局装备,其中 paths
会匹配到本运用。比方这个 @
便是该子运用的 src
,不是大局的。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"common/*": ["../../common/*"]
}
},
"exclude": ["dist", "**/*.js"]
}
vite.config.ts
运用 loadConfigFromFile
和 mergeConfig
,当然其实读取文件,手动合并也是能够的。
import { defineConfig, loadConfigFromFile, mergeConfig } from "vite"
import AutoImport from "unplugin-auto-import/vite"
import packageConfig from "./package.json"
import qiankun from "vite-plugin-qiankun"
import { resolve } from "path"
const pathResolve = (dir: string): string => {
return resolve(__dirname, ".", dir)
}
const useDevMode = true // 假如是在主运用中加载子运用 vite, 有必要翻开这个, 否则vite加载不成功, 独自运转没影响
// https://vitejs.dev/config/
export default defineConfig(async ({ command, mode }) => {
const base = (await loadConfigFromFile({ command, mode }, pathResolve("../../vite.config.ts"))).config
base.plugins.shift()
const config = {
resolve: {
alias: {
"@": pathResolve("src"),
common: pathResolve("../../common")
}
},
server: { port: 10002 },
plugins: [
qiankun(`${packageConfig.name}`, { useDevMode }),
AutoImport({
// vue函数的主动导入
imports: ["vue", "vue-router", "pinia", "@vueuse/core"],
dts: false
})
]
}
return mergeConfig(base, config)
})
windi.config.ts
运用 presets
承继大局装备。
import { defineConfig } from "windicss/helpers"
export default defineConfig({
presets: [require("../../windi.config.ts").default]
})
运用指令行生成子运用
尽管创立子运用不是一个高频操作,但仍是有许多重复操作,并且,每个子运用的模板也不好统一。
所以咱们要规划一个指令行能够主动生成子运用。那么大约需求几步
- 依据
name
,port
注册子运用 - 依据
name
在packages
下新建项目,从git
库房上拉一个模板下来,修正name
和port
之类的装备。 -
pnpm install
并提示
pnpm i -W -D download-git-repo fs-extra inquirer
- download-git-repo
从 git clone 库房到本地
- fs-extra
一个扩展的 fs
- inquirer
能够依据预设收集用户输入,这里用来承认子运用的 name, port
创立提示用户输入
const apps = await readAppsFile()
// console.log(apps)
const meta = await inquirer.prompt([
{
type: "input",
message: "请输入你要新建微运用姓名(英文或许数字):",
name: "appName",
validate(answer) {
console.log(answer)
const done = this.async()
const validateRes = RegxMap.IS_APP_NAME.test(answer)
if (!validateRes) {
done("请按要求输入正确的微运用姓名!")
return
}
if (apps.find(item => item.name === answer)) {
done("已存在同名微运用,请承认后替换姓名再重试。")
return
}
done(null, true)
}
},
{
type: "input",
message: "请输入你要新建的微运用的端口(数字,引荐 10000 - 10010):",
name: "port",
validate(answer) {
const done = this.async()
const validateRes = RegxMap.IS_PORT.test(answer)
if (!validateRes) {
done("只能输入数字!")
return
}
// eslint-disable-next-line eqeqeq
if (apps.find(item => item.port == answer)) {
done("已存在相同微运用端口,请承认后替换再重试。")
return
}
done(null, true)
}
}
])
return meta
用户输入成功之后,从 git
下载模板,正则匹配对应的要害字,替换为用户输入字段
module.exports = async meta => {
await cloneRepo(meta)
await replaceName(meta)
const newApps = await writeAppsFile(meta)
await writeMicroApps(newApps)
const workerProcess = exec(`pnpm install`)
workerProcess.stdout.on("data", data => {
console.log("stdout: " + data)
})
workerProcess.stderr.on("data", data => {
console.log("stderr: " + data)
})
console.log(`微服务创立成功,请在 pnpm install 完结之后,前往 packages/${meta.appName} 目录进行开发`)
}
下面是修正注册子运用信息的代码,其他修正大同小异
// 修正 modules/microApps.ts
const writeMicroApps = async newApps => {
const reg = /const\sapps:\smicroApp\[\]\s=\s\[(.|\s)*export\sdefault\sapps/
const filePath = `../../common/modules/microApps.ts`
const microAppsFile = fs.readFileSync(resolve(__dirname, filePath), "utf-8")
const microAppsContent = microAppsFile.replace(reg, `const apps: microApp[] = ${newApps} \r\n export default apps`)
fs.writeFile(resolve(__dirname, filePath), microAppsContent, err => {
if (err) console.log(err)
})
}
我这里是用一个 JSON 记录了所有子运用信息,当然也能够用正则去匹配 fs 读取的注册信息
增加指令,测验一下。
"gen": "node ./scripts/genNewMicroApp/index.js"
因为 Menu
的逻辑没有写,手动改下路由看下。
其它问题
代码库房: github.com/zhl1232/qia…
模板库房:github.com/zhl1232/qia…
- 一些业务逻辑没有写,比方
Menu
逻辑,大局主题之类的 - 现在子运用模板只要
Vue
- 只要开发环境,没有布置装备
- 库房行尾是用的
linux
装备,windows
环境自行更改Prettier
下的endOfLine
等等吧,假如有需求的话,后面再补吧。
引荐
VSCode
开发的话,能够试下这个插件
GitHub Copilot
便是最近很火的那个 AI 写代码的。挺有意思的,一方面,简略的代码能够靠提示就行,另一方面,假如思路卡住了,他或许还真能给你一些提示。
Code Spell Checker
一个单词拼写查看。语法,风格都有查看的,但实际上单词拼错了也会影响项目质量。不信你装上看一下,基本每个页面都有拼错的单词。。。