vite基础

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],
  },
};

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值