Vite
- Vite (法语意为 “快速的”,发音 /vit/) 是下一代前端开发与构建工具
- 💡 极速的服务启动 使用原生 ESM 文件,无需打包!
- ⚡️ 轻量快速的热重载 无论应用程序大小如何,都始终极快的模块热重载(HMR)
- 🛠️ 丰富的功能 对 TypeScript、JSX、CSS 等支持开箱即用。
- 📦 优化的构建 可选 “多页应用” 或 “库” 模式的预配置 Rollup 构建
- 🔩 通用的插件 在开发和构建之间共享 Rollup-superset 插件接口。
- 🔑 完全类型化的API 灵活的 API 和完整 TypeS
配置开发环境
安装依赖
npm install vue --save
npm install @vitejs/plugin-vue @vue/compiler-sfc vite --save-dev
配置文件
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})
package.json
package.json
{
"name": "vite2-prepare",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.0.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.2.4",
"@vue/compiler-sfc": "^3.0.5",
"vite": "^2.4.0"
}
}
index.html
index.html
<!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>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src\main.js
src\main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
src\App.vue
src\App.vue
<template>
<img src="./assets/ico.jpg" />
<HelloWorld msg="Vue3 + Vite" />
</template>
<script setup>
//https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
import HelloWorld from './components/HelloWorld.vue'
</script>
HelloWorld.vue
src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
</template>
静态资源处理
- 静态资源处理
- 服务时引入一个静态资源会返回解析后的公共路径
模板中引入
src\App.vue
<template>
+ <img src="./assets/avatar.jpg" />
</template>
JS中引入
<template>
<img src="./assets/avatar.jpg" />
+ <img :src="imgUrl" />
<HelloWorld msg="Hello Vue 3 + Vite" />
</template>
<script setup>
//https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
import HelloWorld from './components/HelloWorld.vue'
+import imgUrl from './assets/avatar.jpg'
</script>
CSS中引入
<template>
<img src="./assets/avatar.jpg" />
<img :src="imgUrl" />
+ <div class="avatar"></div>
<HelloWorld msg="Hello Vue 3 + Vite" />
</template>
<script setup>
//https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
import HelloWorld from './components/HelloWorld.vue'
import imgUrl from './assets/avatar.jpg'
</script>
+<style scoped>
+.avatar{
+ width:200px;
+ height:200px;
+ background-image: url(./assets/avatar.jpg);
+ background-size: contain;
+}
+</style>
public目录
- public目录
- 如果有以下需求
- 这些资源不会被源码引用(例如 robots.txt)
- 这些资源必须保持原有文件名(没有经过 hash)
- 那么你可以将该资源放在指定的
public
目录中,它应位于你的项目根目录 - 该目录中的资源在开发时能直接通过
/
根路径访问到,并且打包时会被完整复制到目标目录的根目录下
public\avatar.jpg
配置别名
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
+import {resolve} from 'path';
// https://vitejs.dev/config/
export default defineConfig({
+ resolve:{
+ alias:{
+ '@':resolve('src')
+ }
+ },
plugins: [vue()]
})
App.vue
src\App.vue
<template>
+ <img src="@/assets/avatar.jpg" />
<img :src="avatarUrl" />
<div class="avatar"></div>
<HelloWorld msg="Hello Vue 3 + Vite" />
</template>
<script setup>
//https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
+import HelloWorld from "@/components/HelloWorld.vue";
+import avatarUrl from "@/assets/avatar.jpg";
</script>
<style scoped>
.avatar {
width: 200px;
height: 200px;
+ background-image: url(@/assets/avatar.jpg);
background-size: contain;
}
</style>
样式处理
全局样式
src\main.js
import { createApp } from 'vue'
import App from './App.vue'
+import './global.css'
createApp(App).mount('#app')
src\global.css
#app {
background-color: lightgrey;
}
局部样式
scoped
- 当
<style>
标签有scoped
属性时,它的 CSS 只作用于当前组件中的元素 - 它使用了
data-v-hash
的方式来使css有了它对应模块的标识
src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
+ <a>超链接</a>
</template>
+<style scoped>
+a {
+ color: #42b983;
+}
+</style>
CSS Modules
- CSS Modules
- 通过
module
作用的style
都被保存到$style
对象中
内联
src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
+ <a :class="$style.link">超链接</a>
</template>
+<style module>
+.link {
+ color: #42b983;
+}
+</style>
外联
- 任何以 .module.css 为后缀名的 CSS 文件都被认为是一个 CSS modules 文件
- 导入这样的文件会返回一个相应的模块对象 src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
+ <a :class="style.link">超链接</a>
</template>
<script setup>
+import style from './HelloWorld.module.css';
</script>
src\components\HelloWorld.module.css
.link {
color: #42b983;
}
less和sass
- Vite 也同时提供了对 .scss, .sass, .less, .styl 和 .stylus 文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖
- 如果是用的是单文件组件,可以通过
style lang="sass"
(或其他预处理器)自动开启
安装
npm i less sass -S
HelloWorld.vue
src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
<a :class="style.link">超链接</a>
+ <h2>less</h2>
+ <h3>sass</h3>
</template>
<script setup>
import { reactive } from 'vue'
import style from './HelloWorld.module.css';
</script>
+<style scoped lang="less">
+@color:red;
+h2{
+ color:@color;
+}
+</style>
+<style scoped lang="scss">
+$color:green;
+h3{
+ color:$color;
+}
</style>
PostCSS
- postcss
- 如果项目包含有效的 PostCSS 配置 (任何受 postcss-load-config 支持的格式,例如 postcss.config.js),它将会自动应用于所有已导入的 CSS
安装
npm install autoprefixer --save
postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
.browserslistrc
>0.2%
not dead
not op_mini all
HelloWorld.vue
src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
<a :class="style.link">超链接</a>
<h2>less</h2>
<h3>sass</h3>
+ <div class="postcss"></div>
</template>
<script setup>
import { reactive } from 'vue'
import style from './HelloWorld.module.css';
</script>
<style scoped lang="less">
@color:red;
h2{
color:@color;
}
</style>
<style scoped lang="scss">
$color:green;
h3{
color:$color;
}
</style>
+<style scoped>
+.postcss{
+ height:50px;
+ width:200px;
+ background-color: orange;
+ transform: rotate(90deg);
+}
+</style>
typescript
安装
cnpm install typescript @babel/core @babel/preset-env @babel/preset-typescript --save-dev
babelrc
.babelrc
{
"presets": [
["@babel/preset-env"],
"@babel/preset-typescript"
]
}
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
HelloWorld.vue
src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
<h2>less</h2>
<h3>sass</h3>
<div class="postcss"></div>
+ <button @click="handleClick">{{state.count}}</button>
</template>
<script setup lang="ts">
import { reactive,defineProps } from 'vue'
+defineProps({
+ msg:String
+})
+interface State {
+ count:number;
+}
+let state = reactive<State>({count:0});
+const handleClick = ()=>{
+ console.log(state.count);
+ state.count++;
+}
</script>
<style scoped lang="less">
@color:red;
h2{
color:@color;
}
</style>
<style scoped lang="scss">
$color:green;
h3{
color:$color;
}
</style>
<style scoped>
.postcss{
height:50px;
width:200px;
background-color: orange;
transform: rotate(90deg);
}
</style>
shims-vue.d.ts
- 让typescript识别支持
.vue
文件
src\shims-vue.d.ts
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
vite-env.d.ts
- 如果你的库依赖于某个全局库,使用/// 指令
- 三斜线指令仅可放在包含它的文件的最顶端
- 三斜线引用告诉编译器在编译过程中要引入的额外的文件
src\vite-env.d.ts
/// <reference types="vite/client" />
实现vue插件
vite.config.js
vite.config.js
import { defineConfig } from "vite";
-import vue from "@vitejs/plugin-vue";
+import vue from "./plugins/plugin-vue";
export default defineConfig({
plugins: [vue({})]
});
plugin-vue.js
plugins\plugin-vue.js
import { createFilter, normalizePath } from '@rollup/pluginutils';
import { parse, compileScript, rewriteDefault, compileTemplate, compileStyleAsync } from 'vue/compiler-sfc';
import hash from 'hash-sum';
import path from 'path';
import fs from 'fs';
const root = process.cwd();
const descriptorCache = new Map();
function vue(pluginOptions) {
const { include = /\.vue$/, exclude } = pluginOptions;
const filter = createFilter(include, exclude);
return {
name: 'vue',
async load(id) {
//.log('id', id);//C:\aproject\16.viteplugin\src\App.vue
const { filename, query } = parseVueRequest(id);
if (!filter(filename)) {
return null;
}
if (query.has('vue')) {
const descriptor = await getDescriptor(filename);
if (query.get('type') === 'style') {
let block = descriptor.styles[Number(query.get('index'))];
if (block) {
return { code: block.content };
}
}
}
},
async transform(code, id) {
const { filename, query } = parseVueRequest(id);
if (!filter(filename)) {
return null;
}
if (query.get('type') === 'style') {
const descriptor = await getDescriptor(filename);
let result = await transformStyle(code, descriptor, query.get('index'));
return result;
} else {
let result = await transformMain(code, filename);
return result;
}
}
}
}
async function transformStyle(code, descriptor, index) {
const block = descriptor.styles[index];
//如果是CSS,其实翻译之后和翻译之前内容是一样的,最终返回的JS靠packages\vite\src\node\plugins\css.ts
const result = await compileStyleAsync({
filename: descriptor.filename,
source: code,
id: `data-v-${descriptor.id}`,//必须传递,不然报错
scoped: block.scoped
});
let styleCode = result.code;
return {
code: styleCode
};
/* let styleScript = `
let style = document.createElement('style');
style.innerText = ${JSON.stringify(styleCode)};
document.head.appendChild(style);
`;
return {
code: styleScript
}; */
}
async function transformMain(source, filename) {
const descriptor = await getDescriptor(filename, source);
const scriptCode = genScriptCode(descriptor, filename);
const templateCode = genTemplateCode(descriptor, filename);
const stylesCode = genStyleCode(descriptor, filename);
let resolveCode = [
stylesCode,
templateCode,
scriptCode,
`_sfc_main.render=render`,
`export default _sfc_main`
].join('\n');
return {
code: resolveCode
}
}
function genStyleCode(descriptor, filename) {
let styleCode = '';
if (descriptor.styles.length) {
descriptor.styles.forEach((style, index) => {
const query = `?vue&type=style&index=${index}&lang=css`;
const styleRequest = normalizePath(filename + query);// /
styleCode += `\nimport ${JSON.stringify(styleRequest)}`;
});
return styleCode;
}
}
function genTemplateCode(descriptor, filename) {
let result = compileTemplate({ source: descriptor.template.content, id: filename });
return result.code;
}
/**
* 获取此.vue文件编译 出来的js代码
* @param {*} descriptor
* @param {*} filename
*/
function genScriptCode(descriptor, filename) {
let scriptCode = '';
let script = compileScript(descriptor, { id: filename });
scriptCode = rewriteDefault(script.content, '_sfc_main');//export default => const _sfc_main
return scriptCode;
}
async function getDescriptor(filename, source) {
let descriptor = descriptorCache.get(filename);
if (descriptor) return descriptor;
const content = await fs.promises.readFile(filename, 'utf8');
const result = parse(content, { filename });
descriptor = result.descriptor;
descriptor.id = hash(path.relative(root, filename));
descriptorCache.set(filename, descriptor);
return descriptor;
}
function parseVueRequest(id) {
const [filename, querystring = ''] = id.split('?');
let query = new URLSearchParams(querystring);
return {
filename, query
};
}
export default vue;
实现jsx插件
安装
pnpm install @vitejs/plugin-vue-jsx --save-dev
pnpm install @vue/babel-plugin-jsx @babel/plugin-syntax-import-meta @rollup/pluginutils @babel/plugin-transform-typescript hash-sum morgan fs-extra --save-dev
vite.config.js
vite.config.js
import { defineConfig } from "vite";
-import vue from "@vitejs/plugin-vue";
-import vue from "./plugins/plugin-vue";
+import vueJsx from "./plugins/plugin-vue-jsx.js";
export default defineConfig({
+ plugins: [vueJsx({})]
});
plugin-vue-jsx.js
plugins\plugin-vue-jsx.js
import { transformSync } from '@babel/core'
import jsx from '@vue/babel-plugin-jsx'
import importMeta from '@babel/plugin-syntax-import-meta'
import { createFilter } from '@rollup/pluginutils'
import typescript from '@babel/plugin-transform-typescript';
function vueJsxPlugin(options = {}) {
let root;
return {
name: 'vite:vue-jsx',
config() {
return {
esbuild: {
//默认情况下在开发的时候会编译我们的代码,它会也会编译jsx,但是它会编译 成React.createElement
include: /\.ts$/
},
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}
}
},
configResolved(config) {
root = config.root
},
transform(code, id) {
const {
include,
exclude,
babelPlugins = [],
...babelPluginOptions
} = options
const filter = createFilter(include || /\.[jt]sx$/, exclude)
const [filepath] = id.split('?')
if (filter(id) || filter(filepath)) {
const plugins = [importMeta, [jsx, babelPluginOptions], ...babelPlugins]
if (id.endsWith('.tsx') || filepath.endsWith('.tsx')) {
plugins.push([
typescript,
{ isTSX: true, allowExtensions: true }
])
}
const result = transformSync(code, {
babelrc: false,
configFile: false,
ast: true,
plugins
})
return {
code: result.code,
map: result.map
}
}
}
}
}
export default vueJsxPlugin;
main.js
src\main.js
import { createApp } from 'vue';
+import App from './App.jsx';
createApp(App).mount("#app");
src\App.jsx
src\App.jsx
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return () => (
<h1>App</h1>
)
}
})
HMR
更新消息
{
"type":"update",
"updates":[
{"type":"js-update","timestamp":1647485594371,"path":"/src/App.jsx","acceptedPath":"/src/App.jsx"}
]}
热更新
import { transformSync } from '@babel/core'
import jsx from '@vue/babel-plugin-jsx'
import importMeta from '@babel/plugin-syntax-import-meta'
import { createFilter } from '@rollup/pluginutils'
import typescript from '@babel/plugin-transform-typescript';
+import hash from 'hash-sum'
+import path from 'path'
function vueJsxPlugin(options = {}) {
+ let needHmr = false
return {
name: 'vite:vue-jsx',
config() {
return {
esbuild: {
include: /\.ts$/
},
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}
}
},
configResolved(config) {
root = config.root
+ needHmr = config.command === 'serve' && !config.isProduction
},
transform(code, id) {
const {
include,
exclude,
babelPlugins = [],
...babelPluginOptions
} = options
const filter = createFilter(include || /\.[jt]sx$/, exclude)
const [filepath] = id.split('?')
if (filter(id) || filter(filepath)) {
const plugins = [importMeta, [jsx, babelPluginOptions], ...babelPlugins]
if (id.endsWith('.tsx') || filepath.endsWith('.tsx')) {
plugins.push([
typescript,
{ isTSX: true, allowExtensions: true }
])
}
const result = transformSync(code, {
babelrc: false,
configFile: false,
ast: true,
plugins
})
+ if (!needHmr) {
return { code: result.code, map: result.map }
+ }
+ const hotComponents = []
+ let hasDefault = false
+ for (const node of result.ast.program.body) {
+ if (node.type === 'ExportDefaultDeclaration') {
+ if (isDefineComponentCall(node.declaration)) {
+ hasDefault = true
+ hotComponents.push({
+ local: '__default__',
+ exported: 'default',
+ id: hash(id + 'default')
+ })
+ }
+ }
+ }
+ if (hotComponents.length) {
+ if (hasDefault && (needHmr)) {
+ result.code =
+ result.code.replace(
+ /export default defineComponent/g,
+ `const __default__ = defineComponent`
+ ) + `\nexport default __default__`
+ }
+ if (needHmr && !/\?vue&type=script/.test(id)) {
+ let code = result.code
+ let callbackCode = ``
+ for (const { local, exported, id } of hotComponents) {
+ code +=
+ `\n${local}.__hmrId = "${id}"` +
+ `\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})`
+ callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`
+ }
+ code += `\nimport.meta.hot.accept(({${hotComponents
+ .map((c) => `${c.exported}: __${c.exported}`)
+ .join(',')}}) => {${callbackCode}\n})`
+ result.code = code
+ }
+ }
+ return {
+ code: result.code,
+ map: result.map
+ }
}
}
}
}
function isDefineComponentCall(node) {
return (
node &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'defineComponent'
)
}
export default vueJsxPlugin;
SSR
SSR-html
ssr.js
import express from "express";
import { createServer } from 'vite'
const app = express();
; (async function () {
const vite = await createServer({
server: {
middlewareMode: 'html'
}
})
app.use(vite.middlewares);
app.listen(8000, () => console.log('ssr server started on 8000'))
})();
SSR-ssr
entry-client.js
src\entry-client.js
import { createApp } from './main'
const { app, router } = createApp()
router.isReady().then(() => {
app.mount('#app')
})
entry-server.js
src\entry-server.js
import { createApp } from './main'
import { renderToString } from '@vue/server-renderer'
export async function render(url, manifest = {}) {
const { app, router } = createApp()
router.push(url)
await router.isReady()
const ctx = {}
const html = await renderToString(app, ctx)
const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
return [html, preloadLinks]
}
function renderPreloadLinks(modules, manifest) {
let links = ''
const seen = new Set()
modules.forEach((id) => {
const files = manifest[id]
if (files) {
files.forEach((file) => {
if (!seen.has(file)) {
seen.add(file)
links += renderPreloadLink(file)
}
})
}
})
return links
}
function renderPreloadLink(file) {
console.log('file', file);
if (file.endsWith('.js') || file.endsWith('.jsx')) {
return `<link rel="modulepreload" crossorigin href="${file}">`
} else if (file.endsWith('.css')) {
return `<link rel="stylesheet" href="${file}">`
} else {
return ''
}
}
src\main.js
src\main.js
import App from './App.jsx'
import { createSSRApp } from 'vue'
import { createRouter } from './router'
export function createApp() {
const app = createSSRApp(App)
const router = createRouter()
app.use(router)
return { app, router }
}
src\router.js
src\router.js
import {
createMemoryHistory,
createRouter as _createRouter,
createWebHistory
} from 'vue-router'
const pages = import.meta.glob('./pages/*.jsx')
const routes = Object.keys(pages).map((path) => {
const name = path.match(/\.\/pages(.*)\.jsx$/)[1].toLowerCase()
return {
path: name === '/home' ? '/' : name,
component: pages[path]
}
})
export function createRouter() {
return _createRouter({
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
routes
})
}
src\App.jsx
src\App.jsx
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return () => (
<div>
<ul>
<li><router-link to="/">Home</router-link></li>
<li><router-link to="/user">User</router-link></li>
</ul>
<router-view></router-view>
</div>
)
}
})
src\pages\Home.jsx
src\pages\Home.jsx
import { defineComponent } from 'vue';
import { useSSRContext } from "vue"
export default defineComponent({
setup() {
const ssrContext = useSSRContext()
console.log(ssrContext.modules);
return (props, ctx) => {
console.log('props', props);
console.log('ctx', ctx);
return <h1>Home</h1>;
}
}
})
src\pages\User.jsx
src\pages\User.jsx
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return () => (
<h1>User</h1>
)
}
})
server.js
server.js
import express from "express";
import logger from 'morgan';
import { createServer } from 'vite'
import fs from 'fs-extra';
import path from 'path';
const app = express();
; (async function () {
const vite = await createServer({
server: {
middlewareMode: 'ssr'
}
})
let manifest = JSON.parse(fs.readFileSync(path.resolve('dist/client/ssr-manifest.json'), 'utf-8'))
app.use(vite.middlewares);
app.use(logger('dev'));
app.use('*', async (req, res) => {
const url = req.originalUrl
try {
// 1. 读取 index.html
let template = fs.readFileSync(path.resolve('index.html'), 'utf-8')
// 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端,
// 同时也会从 Vite 插件应用 HTML 转换。
// 例如:@vitejs/plugin-react-refresh 中的 global preambles
template = await vite.transformIndexHtml(url, template)
// 3. 加载服务器入口。vite.ssrLoadModule 将自动转换
// 你的 ESM 源码使之可以在 Node.js 中运行!无需打包
// 并提供类似 HMR 的根据情况随时失效。
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
// 4. 渲染应用的 HTML。这假设 entry-server.js 导出的 `render`
// 函数调用了适当的 SSR 框架 API。
// 例如 ReactDOMServer.renderToString()
const [appHtml, preloadLinks] = await render(url, manifest)
// 5. 注入渲染后的应用程序 HTML 到模板中。
const html = template
.replace(`<!--preload-links-->`, preloadLinks)
.replace(`<!--app-html-->`, appHtml)
// 6. 返回渲染后的 HTML。
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
// 如果捕获到了一个错误,让 Vite 来修复该堆栈,这样它就可以映射回
// 你的实际源码中。
vite.ssrFixStacktrace(e)
console.error(e)
res.status(500).end(e.message)
}
})
app.listen(8000, () => console.log('ssr server started on 8000'))
})();
plugin-vue-jsx.js
plugins\plugin-vue-jsx.js
import { transformSync } from '@babel/core'
import jsx from '@vue/babel-plugin-jsx'
import importMeta from '@babel/plugin-syntax-import-meta'
import { createFilter, normalizePath } from '@rollup/pluginutils'
import typescript from '@babel/plugin-transform-typescript';
import hash from 'hash-sum'
const path = require('path')
const ssrRegisterHelperId = '/__vue-jsx-ssr-register-helper'
const ssrRegisterHelperCode =
`import { useSSRContext } from "vue"\n` +
`export ${ssrRegisterHelper.toString()}`
function ssrRegisterHelper(comp, filename) {
const setup = comp.setup
comp.setup = (props, ctx) => {
// @ts-ignore
const ssrContext = useSSRContext()
; (ssrContext.modules || (ssrContext.modules = new Set())).add(filename)
if (setup) {
return setup(props, ctx)
}
}
}
function vueJsxPlugin(options = {}) {
let root;
let needHmr = false
return {
name: 'vite:vue-jsx',
config() {
return {
esbuild: {
include: /\.ts$/
},
define: {
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}
}
},
configResolved(config) {
root = config.root
needHmr = config.command === 'serve' && !config.isProduction
},
transform(code, id, { ssr }) {
console.log('ssr', ssr);
const {
include,
exclude,
babelPlugins = [],
...babelPluginOptions
} = options
const filter = createFilter(include || /\.[jt]sx$/, exclude)
const [filepath] = id.split('?')
if (filter(id) || filter(filepath)) {
const plugins = [importMeta, [jsx, babelPluginOptions], ...babelPlugins]
if (id.endsWith('.tsx') || filepath.endsWith('.tsx')) {
plugins.push([
typescript,
{ isTSX: true, allowExtensions: true }
])
}
const result = transformSync(code, {
babelrc: false,
configFile: false,
ast: true,
plugins
})
if (!needHmr) {
return { code: result.code, map: result.map }
}
const hotComponents = []
let hasDefault = false
for (const node of result.ast.program.body) {
if (node.type === 'ExportDefaultDeclaration') {
if (isDefineComponentCall(node.declaration)) {
hasDefault = true
hotComponents.push({
local: '__default__',
exported: 'default',
id: hash(id + 'default')
})
}
}
}
if (hotComponents.length) {
if (hasDefault && (needHmr)) {
result.code =
result.code.replace(
/export default defineComponent/g,
`const __default__ = defineComponent`
) + `\nexport default __default__`
}
if (needHmr && !/\?vue&type=script/.test(id)) {
let code = result.code
let callbackCode = ``
for (const { local, exported, id } of hotComponents) {
code +=
`\n${local}.__hmrId = "${id}"` +
`\n__VUE_HMR_RUNTIME__.createRecord("${id}", ${local})`
callbackCode += `\n__VUE_HMR_RUNTIME__.reload("${id}", __${exported})`
}
code += `\nimport.meta.hot.accept(({${hotComponents
.map((c) => `${c.exported}: __${c.exported}`)
.join(',')}}) => {${callbackCode}\n})`
result.code = code
}
}
if (ssr) {
const normalizedId = normalizePath(path.relative(root, id))
let ssrInjectCode =
`\nimport { ssrRegisterHelper } from "${ssrRegisterHelperId}"` +
`\nconst __moduleId = ${JSON.stringify(normalizedId)}`
git for (const { local } of hotComponents) {
ssrInjectCode += `\nssrRegisterHelper(${local}, __moduleId)`
}
result.code += ssrInjectCode
}
return {
code: result.code,
map: result.map
}
}
}
}
}
function isDefineComponentCall(node) {
return (
node &&
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'defineComponent'
)
}
export default vueJsxPlugin;
配置代理
- server-proxy
- 为开发服务器配置自定义代理规则
- 期望接收一个 { key: options } 对象。如果 key 值以 ^ 开头,将会被解释为 RegExp。configure 可用于访问 proxy 实例。
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': resolve('src')
}
},
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://jsonplaceholder.typicode.com',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '')
+ }
+ }
+ },
plugins: [vue()]
})
src\App.vue
src\App.vue
<template>
<img src="@/assets/avatar.jpg" />
<img :src="avatarUrl" />
<div class="avatar"></div>
<HelloWorld msg="Hello Vue 3 + Vite" />
</template>
<script setup>
//https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md
import HelloWorld from "@/components/HelloWorld.vue";
import avatarUrl from "@/assets/avatar.jpg";
+fetch('/api/todos/1')
+ .then(response => response.json())
+ .then(json => console.log(json))
</script>
<style scoped>
.avatar {
width: 200px;
height: 200px;
background-image: url(@/assets/avatar.jpg);
background-size: contain;
}
</style>
mock
npm i mockjs vite-plugin-mock -D
node ./node_modules/vite-plugin-mock/node_modules/esbuild/install.js
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path';
+import { viteMockServe } from "vite-plugin-mock";
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': resolve('src')
}
},
server: {
proxy: {
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
+ plugins: [vue(),viteMockServe({})]
})
mock\test.ts
mock\test.ts
import { MockMethod } from 'vite-plugin-mock';
export default [
{
url: '/api/get',
method: 'get',
response: ({ query }) => {
return {
code: 0,
data: {
name: 'vben',
},
};
},
},
] as MockMethod[];
ESLint
- ESLint是一个开源的 JavaScript 的 linting 工具
- 代码质量问题:使用方式有可能有问题
- 代码风格问题:风格不符合一定规则
npm install eslint eslint-plugin-vue @vue/eslint-config-typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
src\components\HelloWorld.vue
src\components\HelloWorld.vue
<template>
<h1>{{ msg }}</h1>
<h2>less</h2>
<h3>sass</h3>
<div class="postcss" />
<button @click="handleClick">
{{ state.count }}
</button>
</template>
<script setup lang="ts">
import { reactive,defineProps } from 'vue'
defineProps({
+ msg:{
+ type:String,
+ default:''
+ }
})
interface State {
count:number;
}
let state = reactive<State>({count:0});
const handleClick = ()=>{
console.log(state.count);
state.count++;
}
</script>
<style scoped lang="less">
@color:red;
h2{
color:@color;
}
</style>
<style scoped lang="scss">
$color:green;
h3{
color:$color;
}
</style>
<style scoped>
.postcss{
height:50px;
width:200px;
background-color: orange;
transform: rotate(90deg);
}
</style>
main.ts
src\main.ts
import { createApp } from 'vue'
import App from './App.vue'
import './global.css'
createApp(App).mount('#app')
.eslintrc.js
.eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/typescript/recommended"
],
parserOptions: {
ecmaVersion: 2021
},
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
}
}
.eslintignore
.eslintignore
*.css
*.jpg
*.jpeg
*.png
*.gif
*.d.ts
package.json
package.json
{
"name": "hs-vite2-prepare",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
+ "lint":"eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --quiet",
+ "lint:fix":"eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --fix"
},
"dependencies": {
"less": "^4.1.1",
"sass": "^1.35.2",
"vue": "^3.0.5"
},
"devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^4.28.2",
+ "@typescript-eslint/parser": "^4.28.2",
"@vitejs/plugin-vue": "^1.2.4",
"@vue/compiler-sfc": "^3.0.5",
+ "@vue/eslint-config-typescript": "^7.0.0",
"autoprefixer": "^10.2.6",
+ "eslint": "^7.30.0",
+ "eslint-plugin-vue": "^7.13.0",
"mockjs": "^1.1.0",
"vite": "^2.4.0",
"vite-plugin-mock": "^2.9.1"
}
}
Prettier
- ESLint 主要解决的是代码质量问题
- 代码质量规则 (code-quality rules)
- no-unused-vars
- no-extra-bind
- no-implicit-globals
- prefer-promise-reject-errors
- 代码风格规则 (code-formatting rules)
- max-len
- no-mixed-spaces-and-tabs
- keyword-spacing
- comma-style
- 代码风格问题需要使用
Prettier
- Prettier 声称自己是一个有主见的代码格式化工具 (opinionated code formatter)
安装
npm install prettier eslint-plugin-prettier @vue/eslint-config-prettier --save-dev
package.json
{
"name": "hs-vite2-prepare",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"lint": "eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --quiet",
"lint:fix": "eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --fix"
},
"dependencies": {
"less": "^4.1.1",
"sass": "^1.35.2",
"vue": "^3.0.5"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"@vitejs/plugin-vue": "^1.2.4",
"@vue/compiler-sfc": "^3.0.5",
+ "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"autoprefixer": "^10.2.6",
"eslint": "^7.30.0",
+ "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.13.0",
"mockjs": "^1.1.0",
+ "prettier": "^2.3.2",
"vite": "^2.4.0",
"vite-plugin-mock": "^2.9.1"
}
}
.eslintrc.js
.eslintrc.js
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/typescript/recommended",
+ "@vue/prettier",
+ "@vue/prettier/@typescript-eslint",
],
parserOptions: {
ecmaVersion: 2021,
},
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
+ "prettier/prettier": ["error", { endOfLine: "auto" }],
},
};
单元测试
安装依赖
cnpm i jest@next babel-jest@next @types/jest vue-jest@next ts-jest@next @vue/test-utils@next --save-dev
package.json
{
"name": "hs-vite2-prepare",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"lint": "eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --quiet",
"lint:fix": "eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --fix"
},
"dependencies": {
"less": "^4.1.1",
"sass": "^1.35.2",
"vue": "^3.0.5"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"@vitejs/plugin-vue": "^1.2.4",
"@vue/compiler-sfc": "^3.0.5",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"autoprefixer": "^10.2.6",
"eslint": "^7.30.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.13.0",
"mockjs": "^1.1.0",
"prettier": "^2.3.2",
"vite": "^2.4.0",
"vite-plugin-mock": "^2.9.1"
}
}
jest.config.js
- vue-jestJest Vue transformer with source map support
- babel-jestBabel jest plugin
- ts-jestA Jest transformer with source map support that lets you use Jest to test projects written in TypeScript
jest.config.js
module.exports = {
testEnvironment: "jsdom",
transform: {
"^.+\\.vue$": "vue-jest",
"^.+\\.jsx?$": "babel-jest",
"^.+\\.tsx?$": "ts-jest",
},
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
testMatch: ["**/tests/**/*.spec.[jt]s"],
};
tests\test.ts
tests\test.ts
import { mount } from '@vue/test-utils'
const MessageComponent = {
template: '<p>{{ msg }}</p>',
props: ['msg']
}
test('displays message', () => {
const wrapper = mount(MessageComponent, {
props: {
msg: 'Hello world'
}
})
expect(wrapper.text()).toContain('Hello world')
})
tsconfig.json
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
+ "types":["vite/client","jest"],
+ "baseUrl": "./",
+ "paths": {
+ "@": ["./src"]
+ }
},
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","tests/**/*.spec.ts", "tests/test.ts"]
}
package.json
package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"lint": "eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --quiet",
"lint:fix": "eslint --ext .ts,vue src/** --no-error-on-unmatched-pattern --fix",
+ "test": "jest --passWithNoTests"
}
}
git hook
- 可以在
git commit
之前检查代码,保证所有提交到版本库中的代码都是符合规范的 - 可以在
git push
之前执行单元测试,保证所有的提交的代码经过的单元测试 - lint-staged用于实现每次提交只检查本次提交所修改的文件
- lint-staged#configuration
注释规范
-
commitlint 推荐我们使用 config-conventional 配置去写 commit
-
提交格式
git commit -m <type>[optional scope]: <description>
- type :用于表明我们这次提交的改动类型,是新增了功能?还是修改了测试代码?又或者是更新了文档?
- optional scope:一个可选的修改范围。用于标识此次提交主要涉及到代码中哪个模块
- description:一句话描述此次提交的主要内容,做到言简意赅
type类型
类型 | 描述 |
---|---|
build | 编译相关的修改,例如发布版本、对项目构建或者依赖的改动 |
chore | 其他修改, 比如改变构建流程、或者增加依赖库、工具等 |
ci | 持续集成修改 |
docs | 文档修改 |
feature | 新特性、新功能 |
fix | 修改bug |
perf | 优化相关,比如提升性能、体验 |
refactor | 代码重构 |
revert | 回滚到上一个版本 |
style | 代码格式修改 |
test | 测试用例修改 |
安装
cnpm i husky lint-staged @commitlint/cli @commitlint/config-conventional --save-dev
配置脚本
prepare
脚本会在npm install
(不带参数)之后自动执行- 当我们执行npm install安装完项目依赖后会执行
husky install
命令,该命令会创建.husky/
目录并指定该目录为git hooks
所在的目录
npm set-script prepare "husky install"
npm run prepare
创建hooks
npx husky add .husky/pre-commit "lint-staged"
npx husky add .husky/commit-msg "npx --no-install commitlint --edit $1"
npx husky add .husky/pre-push "npm run test"
commitlint.config.js
commitlint.config.js
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feature",
"update",
"fixbug",
"refactor",
"optimize",
"style",
"docs",
"chore",
],
],
"type-case": [0],
"type-empty": [0],
"scope-empty": [0],
"scope-case": [0],
"subject-full-stop": [0, "never"],
"subject-case": [0, "never"],
"header-max-length": [0, "always", 72],
},
};