Vue3.0
曾经公布了很长一段时间了,最近也在想着怎么去做工程化的货色,于是就想本人弄一个组件库捎带深刻学习Vue3.0
,于是便有了明天的这篇文章。
Git地址:https://github.com/ZhQuella/AzUIFront
技术栈
包管理工具:
yarn
开发模块:
Vite
Vue 3.0
打包工具:
Gulp
Rollup
单元测试:
Jest
语法工具:
ESLint
语言:
TypeScript
为什么要应用Yarn
看到包管理工具应用的事yarn
或者很多小伙伴不太能了解为什么要应用yarn
,说到这里可能就须要理解一下前端的工程化的概念了,什么是前端的工程化?工程化别离为模块化
、组件化
、规范化
和自动化
。
模块化
模块化
是指将一个文件拆分成多个相互依赖的文件,最初进行对立的打包和加载,这样可能很好的保障高效的多人合作。JavaScript
模块化:CommonJS
、AMD
、CMD
以及ES6 Module
。CSS
模块化:Sass
、Less
、Stylus
、BEM
、CssModules
等。其中预处理器和 BEM 都会有的一个问题就是款式笼罩。资源模块化
:任何资源都能以模块的模式进行加载,目前大部分我的项目中的文件、CSS、图片等都能间接通过 JS 做对立的依赖关系解决
组件化
不同于模块化,模块化是对文件、对代码和资源拆分,而组件化则是对UI层面
的拆分。
组件化并不是前端所特有的,一些其余的语言或者桌面程序等,都具备组件化的先例。确切的说,只有有UI层的展现,就必然有能够组件化的中央。简略来说,组件就是将一段UI款式和其对应的性能作为独立的整体去对待,无论这个整体放在哪里去应用,它都具备一样的性能和款式,从而实现复用,这种整体化的细想就是组件化。不难看出,组件化设计就是为了减少复用性,灵活性,进步零碎设计,从而进步开发效率。
组件零碎是一个重要概念,因为它是一种形象,容许咱们应用小型、独立和通常可复用的组件构建大型利用。咱们会须要对页面进行拆分,将其拆分成一个一个的整机,而后别离去实现这一个个整机,最初再进行组装。
组件化的形象设计是很重要的,不仅减少了复用性进步了工作效率,从某种程度上来说也反馈了程序员对业务和产品设计的了解,一旦有问题或者须要性能扩大时,你就会发现之前的设计是如许的有意义。
规范化
规范化指的是咱们在工程开发初期以及开发期间制订的系列标准,其中又蕴含了:
- 我的项目目录构造
- 对于编码这块的束缚,个别咱们都会采纳一些强制措施,比方
ESLint
、StyleLint
等。 - 联调标准
- 文件命名标准
- 款式治理标准:目前风行的款式治理有
BEM
、Sass
、Less
、Stylus
、CSS Modules
等形式 - 工作流:其中蕴含分支命名标准、代码合并标准等
- 定期代码审查
自动化
从最早先的grunt
、gulp
等,再到目前的webpack
、parcel
。这些自动化工具在自动化合并、构建、打包都能为咱们节俭很多工作。而这些只是前端自动化其中的一部分,前端自动化还蕴含了继续集成、自动化测试等方方面面。
想必大家对与工程化也有了肯定的理解,那么会过头来持续说为什么要应用yarn
,下面说到了模块化
如果咱们想一下,组件的开发与组件文档的展现和组件库的自身是存在所有关联的,然而关联性并没有那么大,能够应用yarn
的工作空间来分隔他们使他们本事作为一个独立的个体而存在,yarn
工作流与npm
相似都是应用package.json
文件,yarn
会重组node_modules
文件,且不会与npm
抵触。yarn
的工作空间可能更好的帮忙咱们治理多个我的项目,能够在多个子项目中应用独立的package.json
治理我的项目依赖。yarn
会依据就依赖关系帮忙你剖析所有子项目的共用依赖,保障所有的我的项目专用的依赖只会被下载和装置一次。
初始化我的项目
初始化创立我的项目:
mkdir AzUiFontcd AzUiFontyarn init -y
创立.gitignore
node_modulescoveragedisteslibpackage-lock.json
批改package.json
{ "name": "az-ui-font", "private": true, "version": "0.0.1", "main": "index.js", "license": "MIT"}
搭建初始目录构造:
├─packages // 我的项目总目录│ ├─azui // 组件库目录│ ├─docs // 展现文档目录│ └─play // 组件库开发展现目录├─script // 运行脚本├─.gitignore ├─index.html ├─package.json ├─README.md └─tsconfig.json
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>AzUIFront</title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> </body></html>
tsconfig.json
{ "compilerOptions": { "rootDir": ".", "sourceMap": false, "target": "esnext", "module": "esnext", "jsx": "preserve", "moduleResolution": "node", "strictNullChecks": true, "noUnusedLocals": true, "resolveJsonModule": true, "esModuleInterop": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "baseUrl": ".", "lib": ["esnext", "dom"], "types": ["jest", "node"] }}
代码标准束缚
这里应用eslint
对代码标准进行束缚,装置如下工具:
yarn add eslint -D -Wyarn add eslint-formatter-pretty -D -Wyarn add eslint-plugin-json -D -Wyarn add eslint-plugin-prettier -D -Wyarn add eslint-plugin-vue -D -Wyarn add @vue/eslint-config-prettier -D -Wyarn add babel-eslint -D -Wyarn add prettier -D -W
增加.eslintrc.js
module.exports = { "root": true, "env": { "browser": true, "es2020": true, "node": true, "jest": true }, "globals": { "ga": true, "chrome": true, "__DEV__": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", 'plugin:json/recommended', 'plugin:vue/vue3-essential', '@vue/prettier' ], "parserOptions": { "ecmaVersion": 12, "parser": "@typescript-eslint/parser", "sourceType": "module" }, "plugins": [ "vue", "@typescript-eslint" ], "rules": { }};
增加.eslintignore
*.sh*.md*.ico*.css*.md*.scss*.woff*.ttf*/*/shims.d.ts */*/index.html*/*/yarn-error.logpackages/azui/rollup-plugin-vue/index.d.tsnode_moduleslibcoveragedist
在package.json
增加如下命令:
package.json
{ "scripts": { "lint": "eslint --no-error-on-unmatched-pattern --ext .vue --ext .ts --ext .tsx packages/**/ --fix" }, "devDependencies": { "eslint": "^7.24.0", "eslint-config-prettier": "^8.3.0", "eslint-formatter-pretty": "^4.0.0", "eslint-plugin-jest": "^24.3.5", "eslint-plugin-json": "^2.1.2", "eslint-plugin-prettier": "^3.4.0", "prettier": "^2.2.1" }}
代码束缚局部曾经解决实现,接下来解决组件局部
组件库局部
执行命令
cd .\packages\azui\yarn init -y
package.json
{ "name": "azui", "version": "0.0.1", "private": true, "license": "MIT"}
目录构造如下
├─rollup-plugin-vue└─src ├─packages │ └─Button │ ├─Button.vue │ ├─index.ts │ └─__tests__ └─styles
集成Babel
到我的项目中,须要装置如下依赖:
yarn add babel -D -Wyarn add babel-plugin-syntax-dynamic-import -D -Wyarn add babel-plugin-syntax-jsx -D -Wyarn add babel-preset-env -D -Wyarn add @babel/plugin-proposal-optional-chaining -D -Wyarn add @babel/preset-env -D -Wyarn add @vue/babel-plugin-jsx -D -W
增加.babelrc
{ "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]], "plugins": [ "syntax-dynamic-import", ["@vue/babel-plugin-jsx"], "@babel/plugin-proposal-optional-chaining", "@babel/plugin-proposal-nullish-coalescing-operator" ], "env": { "utils": { "presets": [ [ "env", { "loose": true, "modules": "commonjs", "targets": { "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] } } ] ], "plugins": [ [ "module-resolver", { "root": ["azui"], "alias": { "azui/src": "az-ui/lib" } } ] ] }, "test": { "plugins": ["istanbul"], "presets": [["env", { "targets": { "node": "current" } }]] }, "esm": { "presets": [["@babel/preset-env", { "modules": false }]] } }}
集成自动化测试(单元测试)
yarn add jest -D -Wyarn add vue-jest@5.0.0-alpha.5 -D -Wyarn add babel-jest -D -W yarn add @vue/compiler-sfc@3.0.2 -D -Wyarn add @vue/test-utils@next -D -Wyarn add typescript -D -W
增加jest.config.js
module.exports = { testEnvironment: "jsdom", // 默认JSdom roots: ["<rootDir>/src/packages"], // transform: { "^.+\\.vue$": "vue-jest", // vue单文件 "^.+\\js$": "babel-jest", // esm最新语法 import }, moduleFileExtensions: ["vue", "js", "json", "jsx", "ts", "tsx", "node"], testMatch: ["**/__tests__/**/*.spec.js"], // 别名 moduleNameMapper: { "^azui(.*)$": "<rootDir>$1", "^main(.*)$": "<rootDir>/src$1", },};
package.json
{ "scripts": { "test": "jest --runInBand" }}
款式打包,增加如下依赖:
yarn add gulp -D -Wyarn add gulp-autoprefixer -D -Wyarn add gulp-sass -D -Wyarn add gulp-cssmin -D -Wyarn add cp-cli -D -Wyarn add tslib -D -W
package.json
{ "scripts": { "build:theme": "gulp build --gulpfile gulpfile.js" }}
应用Rollup打包组件,装置如下依赖:
yarn add rollup -D -Wyarn add rollup-plugin-peer-deps-external -D -Wyarn add rollup-plugin-scss -D -Wyarn add rollup-plugin-terser -D -Wyarn add rollup-plugin-vue -D -Wyarn add @rollup/plugin-node-resolve -D -Wyarn add @rollup/plugin-commonjs -D -Wyarn add @rollup/plugin-json -D -Wyarn add @rollup/plugin-replace -D -Wyarn add @rollup/plugin-babel -D -Wyarn add rollup-plugin-vue -D -Wyarn add rollup-plugin-typescript2 -D -W
增加rollup.config.js
import path from "path";import scss from "rollup-plugin-scss";import peerDepsExternal from "rollup-plugin-peer-deps-external";import resolve from "@rollup/plugin-node-resolve";import commonjs from "@rollup/plugin-commonjs";import json from "@rollup/plugin-json";import replace from "@rollup/plugin-replace";import babel from "@rollup/plugin-babel";import { terser } from "rollup-plugin-terser";import ts from "rollup-plugin-typescript2";import pkg from "./package.json";const vuePlugin = require("./rollup-plugin-vue/index");const getPath = (_path) => path.resolve(__dirname, _path);const name = "AzUi";const createBanner = () => { return `/*! * ${pkg.name} v${pkg.version} * (c) ${new Date().getFullYear()} Aaron * @license MIT */`;};const extensions = [".js", ".ts", ".tsx", ".scss"];const tsPlugin = ts({ tsconfig: getPath("./tsconfig.json"), extensions,});const createBaseConfig = () => { return { input: "src/index.ts", external: ["vue"], plugins: [ peerDepsExternal(), babel(), resolve({ extensions, }), commonjs(), json(), tsPlugin, vuePlugin({ css: true }), scss({ output: process.env.NODE_ENV === 'development'? './dist/lib/index.css': false, watch: ["./src/styles"] }) ], output: { sourcemap: false, banner: createBanner(), externalLiveBindings: false, globals: { vue: "Vue" } } };};function mergeConfig(baseConfig, configB) { const config = Object.assign({}, baseConfig); // plugin if (configB.plugins) { baseConfig.plugins.push(...configB.plugins); } // output config.output = Object.assign({}, baseConfig.output, configB.output); return config;}function createFileName(formatName) { return `dist/az-ui.${formatName}.js`;}// es-bundleconst esBundleConfig = { plugins: [ replace({ __DEV__: `(process.env.NODE_ENV !== 'production')`, }), ], output: { file: createFileName("esm-bundler"), format: "es", },};// es-browserconst esBrowserConfig = { plugins: [ replace({ __DEV__: true, }), ], output: { file: createFileName("esm-browser"), format: "es", },};// es-browser.prodconst esBrowserProdConfig = { plugins: [ terser(), replace({ __DEV__: false, }), ], output: { file: createFileName("esm-browser.prod"), format: "es", },};// cjsconst cjsConfig = { plugins: [ replace({ __DEV__: true, }), ], output: { file: createFileName("cjs"), format: "cjs", },};// cjs.prodconst cjsProdConfig = { plugins: [ terser(), replace({ __DEV__: false, }), ], output: { file: createFileName("cjs.prod"), format: "cjs", },};// globalconst globalConfig = { plugins: [ replace({ __DEV__: true, "process.env.NODE_ENV": true, }), ], output: { file: createFileName("global"), format: "iife", name },};// global.prodconst globalProdConfig = { plugins: [ terser(), replace({ __DEV__: false, }), ], output: { file: createFileName("global.prod"), format: "iife", name },};const formatConfigs = [ esBundleConfig, esBrowserProdConfig, esBrowserConfig, cjsConfig, cjsProdConfig, globalConfig, globalProdConfig,];function createPackageConfigs() { return formatConfigs.map((formatConfig) => { return mergeConfig(createBaseConfig(), formatConfig); });}export default createPackageConfigs();
package.json
{ "scripts": { "build": "rollup -c" }}
组件库局部曾经实现,接下来配置文档局部:
文档局部
执行命令
cd ../..cd .\packages\docs\
创立目录构造:
├─public├─scripts└─src ├─assets ├─components └─__docs__
装置如下依赖:
yarn add @vitejs/plugin-vue -D -wyarn add markdown-it-containe -D -wyarn add node-sass -D -wyarn add sass -D -wyarn add sass-loader -D -wyarn add vite -D -wyarn add vite-plugin-vuedoc -D -wyarn add vue@next -S -Wyarn add vue-router@4 -S -W
package.json
{ "name": "@azui/docs", "version": "0.0.1", "private": true, "scripts": { "dev": "vite", "build": "vite build", "test": "tsrv test" }, "dependencies": { "azui": "0.0.1", "vue": "^3.0.7", "vue-router": "^4.0.4" }, "devDependencies": { "@vitejs/plugin-vue": "^1.1.5", "markdown-it-container": "^3.0.0", "node-sass": "^5.0.0", "sass": "^1.32.11", "sass-loader": "^11.0.1", "vite": "^2.0.5", "vite-plugin-vuedoc": "^3.1.2" }}
增加vite.config.ts
import { defineConfig } from "vite";import { createPlugin, vueDocFiles } from "vite-plugin-vuedoc";import markdownItContainer from "markdown-it-container";import vue from "@vitejs/plugin-vue";import vitePluginSyncmd from "./scripts/vitePluginSyncmd";const containers = ["success", "warning", "info", "error"].map((type) => { return [ markdownItContainer, type, { validate: function (params: string) { const str = params.trim(); if (str === type || str.startsWith(`${type} `)) { return [str, str === type ? "" : str.substr(type.length + 1)]; } return null; }, render: function (tokens: any[], idx: number) { const str = tokens[idx].info.trim(); const m = [str, str === type ? "" : str.substr(type.length + 1)]; if (tokens[idx].nesting === 1) { // opening tag return `<p>${type}--${m[1]}`; } else { // closing tag return "</p>"; } }, }, ];});export default defineConfig({ server: { port: 3000, }, assetsInclude: ["src/assets"], optimizeDeps: { exclude: ["azui"], }, plugins: [ vitePluginSyncmd(), createPlugin({ markdownIt: { plugins: [...containers], }, highlight: { theme: "one-dark", }, }), vue({ include: [...vueDocFiles], }), ],});
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "lib": ["esnext", "dom"], "types": ["vite/client"], "baseUrl": "." }, "include": ["./shims.d.ts", "src/**/*"], "exclude": ["node_modules", "dist"]}
shims.d.ts
declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component}declare module '*.md' { import { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component}
index.html
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" href="/src/assets/icon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite App</title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> </body></html>
scripts/vitePluginSyncmd.ts
import { Plugin } from "vite";import chokidar from "chokidar";import path from "path";import fs from "fs-extra";function docFileName(path: string) { const ret = path.split("/__docs__/"); if (ret.length === 2) { return ret; } return [null, null];}function syncdocServer({ root }) { const componentsDir = path.join(root, "../elenext/src/components"); const docsPath = (file) => path.join(root, "src/__docs__", file); const watcher = chokidar.watch(`${componentsDir}/**/__docs__/*.md`); watcher .on("add", async (path) => { const [, file] = docFileName(path); if (file) { try { await fs.copy(path, docsPath(file)); } catch (err) { console.error(err); } } }) .on("change", async (path) => { const [, file] = docFileName(path); if (file) { try { await fs.copy(path, docsPath(file)); } catch (err) { console.error(err); } } }) .on("unlink", async (path) => { const [, file] = docFileName(path); if (file) { try { await fs.remove(docsPath(file)); } catch (err) { console.error(err); } } });}function vitePluginSyncmd(): Plugin { return { name: "Syncmd", configureServer(server) { syncdocServer({ root: server.config.root }); }, };}export default vitePluginSyncmd;
src/__docs__/Button.zh-CN.md
---title: ButtonwrapperClass: md-button---# Button 按钮罕用的操作按钮## 按钮色彩应用`color`属性来定义 Button 的色彩`color`: 'primary' | 'success' | 'info' | 'warning' | 'danger'## Button Props| 参数 | 阐明 | 类型 | 默认值 || ----------- | -------------- | ---------------------------------------------------------------- | ------ || color | 类型 | 'primary' / 'success' / 'warning' / 'danger' / 'info' / 'string' | - |
components/AppLayout.vue
<template> <div class="demo-layout"> <div> <div class="demo-header"> <div class="layout-center"> <div align="middle"> <div :flex="1"> <Logo /> </div> <div> <div mode="horizontal"> <div> <input v-model="data.primaryColor" /> </div> <div> <a href="https://github.com/JasKang/elenext" target="__blank">GitHub</a> </div> </div> </div> </div> </div> </div> <div> <div class="layout-center"> <div align="top" :wrap="false"> <div :flex="'200px'"> <div style="padding-top: 40px"> <div mode="vertical" :current-path="route.path"> <template v-for="menu in menus" :key="menu.title"> <div :title="menu.title"> <div v-for="item in menu.items" :key="item" :path="`/${item.name.toLowerCase()}`"> {{ `${item.name}-Aaron` }} </div> </div> </template> </div> </div> </div> <div :flex="1"> <div class="site-content"> <router-view /> </div> </div> </div> </div> </div> </div> </div></template><script lang="ts">import { defineComponent, reactive } from 'vue'import { useRoute } from 'vue-router'import menus from '../menus'export default defineComponent({ name: 'AppLayout', setup() { const route = useRoute() const data = reactive({ primaryColor: '#409eff', }) return { route, menus, data, } },})</script><style lang="scss">.demo-layout { height: 100vh;}.layout-center { max-width: 1200px; width: 100vw; margin: 0 auto;}.site-content { width: 100%; padding: 20px; // max-width: 900px; margin: 0 auto;}.demo-aside { border-right: solid 1px #e6e6e6;}.demo-header { border-bottom: solid 1px #e6e6e6; // box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);}</style>
App.vue
<template> <div class="box">111</div> <router-view /></template><script lang="ts">import { defineComponent } from 'vue'export default defineComponent({ name: 'App', components: {}})</script>
main.ts
import 'vite-plugin-vuedoc/style.css';import { createApp } from 'vue';import AzUI from 'azui';import 'azui/dist/lib/index.css';import { router } from './router';import App from './App.vue';const app = createApp(App);app.use(AzUI);app.use(router);app.mount('#app');
menus.ts
import { Component, defineAsyncComponent } from 'vue'import Button from './__docs__/Button.zh-CN.md'type MenuItemType = { name: string component: (() => Promise<Component>) | Component}type MenuType = { title: string items: MenuItemType[]}export default [ { title: 'Basic', items: [ { name: 'Button', component: Button }, ] }] as MenuType[]
router.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'import AppLayout from './components/AppLayout.vue'import menus from './menus'export const router = createRouter({ history: createWebHistory(), strict: true, routes: [ { path: '/'}, { path: '/component', name: 'Layout', component: AppLayout, redirect: '/button', children: menus.reduce((prev, item) => { const _routes = item.items.map(i => { console.log(i.component) return { path: `/${i.name.toLowerCase()}`, name: i.name, component: i.component, } }) prev.push(..._routes) return prev }, [] as RouteRecordRaw[]), }, ],})
文档局部实现,接下来解决组件开发局部,其实组件开发局部能够和文档局部放在一起,应用不同的路由,然而两者的性能不雷同,所以独自配置了一个子项目。
开发局部
执行如下命令:
cd ../..cd .\packages\play\
目录构造:
├─public├─scripts├─index.html└─src ├─App.vue └─main.ts
package.json,相干依赖自行装置
{ "name": "@azui/play", "version": "0.0.1", "private": true, "scripts": { "dev": "vite", "build": "vite build", "test": "tsrv test" }, "dependencies": { "azui": "0.0.1", "vue": "^3.0.7" }, "devDependencies": { "@vitejs/plugin-vue": "^1.1.5", "node-sass": "^5.0.0", "sass": "^1.32.11", "sass-loader": "^11.0.1", "vite": "^2.0.5" }}
shims.d.ts
declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component}declare module '*.md' { import { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component}
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "lib": ["esnext", "dom"], "types": ["vite/client"], "baseUrl": "." }, "include": ["./shims.d.ts", "src/**/*"], "exclude": ["node_modules", "dist"]}
vite.config.ts
import { defineConfig } from "vite";import vue from "@vitejs/plugin-vue";export default defineConfig({ server: { port: 8080, }, optimizeDeps: { exclude: ["azui"], }, plugins: [ vue() ]});
这样的话组件开发局部也就配置好了,当初就是启动我的项目的时候,返回到根目录,在根目录的package.json
中增加scripts
:
我的项目启动
root package.json
{ "scripts": { "dev": "yarn workspace azui run dev", "start": "yarn workspace @azui/docs run dev", "play": "yarn workspace @azui/play run dev", "test": "yarn workspace azui run test", "lint": "eslint --no-error-on-unmatched-pattern --ext .vue --ext .ts --ext .tsx packages/**/ --fix" }}
须要留神的是,肯定要先运行yarn dev
依据dev
来运行打包出开发环境所须要的组件,之后再运行start
或者play
,记的有两个命令行窗口运行哦,如果间接运行start
和play
会抛出谬误:
[plugin:vite:import-analysis] Failed to resolve entry for package "azui". The package may have incorrect main/module/exports specified in its package.json.
这是因为在我的项目中无奈找到所依赖的组件库~这一点须要非凡留神。
结束语
要搭建一个绝对欠缺的组件库,都是须要通过一系列我的项目的积淀的。目前而言,组件库的开发流程上仍然会存在一些问题,比方版本治理、降级回退等。工夫是最好的老师,置信在前面的迭代中,咱们仍然可能放弃初心与激情,积极探索与发现,构建出更加欠缺的前端工程体系。