Vue3+JSX+TS尝鲜

1,842 阅读6分钟

Vue的编译方式变啦

采用vite作为Vue的脚手架开发构建工具

Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题

安装全局脚手架依赖 脚手架gitHub

cnpm install -g create-vite-app

创建脚手架项目

这里不能使用cnpm

npm create vite-app <project-name> 

集成Ts

cnpm i --dev typescript

项目根目录创建配置文件:tsconfig.json

{
  "compilerOptions": {
    /* 指定编译之后的版本目录【指定ECMAScript目标版本】
      "ES3","ES5","ES6","ES2015","ES2016",
      "ES2017","ES2018","ES2019","ES2020","ESNext"
     */
    "target": "esnext",
    /* 指定要使用的模板标准
      "CommonJS","AMD","System","UMD","ES6",
      "ES2015","ES2020","ESNext","None"
    */
    "module": "esnext",
    /* 启用所有严格类型检查选项。需要TypeScript版本2.3或更高版本 
    boolean 
    */
    "strict": true,
    /* jsx代码用于的开发环境
     'preserve', 'react', or 'react-native'
    */
    "jsx": "preserve",
    /* 用来指定是否在编译的时候生成相的d.ts声明文件,
    如果设为true,编译每个ts文件之后会生成一个js文件和一个声明文件,
    但是declaration和allowJs不能同时设为true */
    "declaration": true,
    /* 用来指定编译时是否生成.map文件 */
    "declarationMap": false,
    /* 用来指定编译时是否生成.map文件 */
    "sourceMap": false,
    /* 指定是否引入tslib里的复制工具函数,默认为false */
    "importHelpers": true,
    /*  此处设置为node,才能解析import xx from 'xx' */
    "moduleResolution": "node",
    /* 启用对ES7装饰器的实验性支持 */
    "experimentalDecorators": true,
    "esModuleInterop": true,
    /* 不报告执行不到的代码错误 */
    "allowSyntheticDefaultImports": true,
    // 必须标志Null 类型,才可以赋值为null
    /* 当设为true时,null和undefined值不能赋值给非这两种类型的值,
    别的类型的值也不能赋给他们,除了any类型,
    还有个例外就是undefined可以赋值给void类型 */
    "strictNullChecks": true,
    /*解析 json的扩展 */
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    /* 指定要包含在编译中的库文件 */
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  "include": [
    "src/**/*",
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  "exclude": ["node_modules", "dist"]
}

集成eslint

cnpm i -D eslint eslint-plugin-vue

项目根目录创建配置文件.eslintrc.js

module.exports = {
  parser: "vue-eslint-parser",
  parserOptions: {
    parser: "@typescript-eslint/parser", // 指定ESLint解析 Specifies the ESLint parser
    ecmaVersion: 2020, // 允许解析现代ECMAScript特性 Allows for the parsing of modern ECMAScript features
    sourceType: "module", // 允许使用imports Allows for the use of imports
    ecmaFeatures: {
      tsx: true, //允许解析tsx Allows for the parsing of JSX
      jsx: true
    }
  },
  // settings: {
  //   tsx: {
  //     version: "detect" // Tells eslint-plugin-react to automatically detect the version of React to use
  //   }
  // },
  extends: [
    "plugin:vue/vue3-recommended",
    "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
    "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
    "plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
  ],
  rules: {
    "no-console": process.env.NODE_ENV === "production" ? "error" : "off",
    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
    "comma-dangle": "off", // 修正 eslint-plugin-vue 带来的问题,
    // allow async-await
    "generator-star-spacing": "off",
    "no-undef": "off",
    camelcase: "off"
  }
};

集成pritter

cnpm i -D prettier eslint-config-prettier eslint-plugin-prettier

项目根目录创建配置文件:.prettierrc.js

module.exports = {
  /**
   * @desc 用tab代替空格进行缩进
   * @默认 false
   * @params
   *  - false:使用tab
   *  - true:使用空格【tabWidth无效】
   */
  useTabs: false,
  /**
   * @desc 指定每个tab缩进级别的空格数
   * @默认 2
   * @params number [object,改了该文件会立马格式变掉]
   */
  tabWidth: 2,
  /**
   * @desc 指定每行最多多少个字符才换行
   * @默认 80
   * @params number
   */
  printWidth: 100,
  /**
   * @desc 末尾自动使用分号
   * @default true
   * @params
   *  - true:自动加入`;`分号
   *  - false:自动删除`;`分号
   */
  semi: true,
  /**
   * @desc 多行时【数组,对象等被迫换行的时候】,
   * 尽可能在后面打印逗号。(提示:单行数组的末尾永远不会有逗号)
   * @default es5
   * @params
   *  - es5:在ES5中有效的尾部逗号(对象,数组等)
   *  - none:没有尾随逗号
   *  - all:尽可能在后面加上逗号(包括函数参数)
   */
  trailingComma: 'all',
  /**
   * @desc 使用单引号,来替代双引号
   * @default  false
   * @notes
   *  - JSX文件忽略这个选项--可以看 https://prettier.io/docs/en/options.html#jsx-quotes
   *  - 如果引号的数量超过另一个引号,则使用较少的引号将格式化字符串
   *  ==>示例:"I'm double quoted" 导致  "I'm double quoted"
   *  ==>"This \"example\" is single quoted" 导致 'This "example" is single quoted'
   * @params
   *  - true:使用单引号代替双引号
   *  - false:使用双引号
   */
  singleQuote: true,
  /**
   * @desc 当对象中的属性被引用改变
   * @default as-needed
   * @param
   *  - as-needed:只在需要的时候在对象属性周围添加引号【加不加引号自己决定】
   *  - consistent:对象属性都加入引号
   *  - preserve:尊重对象属性中引号的输入用法
   */
  quoteProps: 'consistent',
  /**
   * @desc 在JSX中使用单引号而不是双引号
   * @default false
   * @params
   *  - false:使用双引号
   *  - true:使用单引号
   */
  jsxSingleQuote: true,
  /**
   * @desc 在对象文字中的括号之间打印空格
   * @default true
   * @params
   *  - false:{foo: bar}
   *  - true:{ foo: bar }
   */
  bracketSpacing: true,
  /**
   * @desc JSX标签闭合位置'>'
   * @default false
   * @params
   *  - false:
   *        <button
              className="prettier-class"
              id="prettier-id"
              onClick={this.handleClick}
            >
              Click Here
            </button>
   *  - true:
   *        <button
              className="prettier-class"
              id="prettier-id"
              onClick={this.handleClick}>
              Click Here
            </button>
   */
  jsxBracketSameLine: true,
  /**
   * @desc 在箭头函数参数周围包含圆括号
   * @default always
   * @params
   *  - always:(x)=>{}
   *  - avoid: x => x
   */
  arrowParens: 'avoid',
  /**
   * @desc 默认情况下,Prettier将按原样包装markdown文本,
   *       因为某些服务使用对换行符敏感的渲染器,
   *       例如 GitHub注释和BitBucket。
   *       在某些情况下,您可能希望改用编辑器/查看器软包装,因此此选项允许您选择“从不”。
   * @default always
   * @params
   *  - always:如果超出printWidth限制的字符,那我们就包装prose【美化代码】
   *  - never: 不使用包装prose【美化代码】
   *  - preserve: 原样包装prose【美化代码】  v1.9.0中首次可用
   */
  proseWrap: 'preserve',
  /**
   * @desc 为HTML文件指定全局空格敏感性,详细可以看
   * https://prettier.io/blog/2018/11/07/1.15.0.html#whitespace-sensitive-formatting
   * @params
   *  - css: 尊重CSS display属性的默认值
   *  - strict:空格被认为是敏感的
   *  - ignore:空格被认为是不敏感的
   */
  htmlWhitespaceSensitivity: 'ignore',
  /**
   * @desc 是否缩进Vue文件中<script>和<style>标记内的代码。
   * 有些人(例如Vue的创建者)不缩进以保存缩进级别,但这可能会破坏编辑器中的代码折叠。
   */
  vueIndentScriptAndStyle: true,
  /**
   * @desc 由于历史原因,文本文件中有两种常见的行结束形式。
   * 即\n(或LF表示换行)和\r\n(或CRLF表示回车+换行)。
   * 前者在Linux和macOS上很常见,而后者在Windows上很普遍。
   * @params
   *  - lf:仅限换行(\n)
   *  - crlf:回车+换行符(\r\n)
   *  - cr:只用回车符(\r)
   *  - auto:维护现有的行结束符
   */
  endOfLine: 'auto',
};

优化TS类型推断

在src目录下

  • 创建source.d.ts【声明静态资源文件】

    declare module '*.json';
    declare module '*.png';
    declare module '*.gif';
    declare module '*.jpg';
    declare module '*.jpeg';
    declare module '*.svg';
    declare module '*.css';
    declare module '*.less';
    declare module '*.scss';
    declare module '*.sass';
    
  • 创建shim.d.ts

    declare module '*.vue' {
      import Vue from 'vue';
      export default Vue;
    }
    declare module '*.tsx' {
      import Vue from 'vue';
      export default Vue;
    }
    

安装vue-router

cnpm i -D vue-router@4.0.0-beta.2
  • 在src目录下,新建router文件夹

    index.ts

    import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router';
    
    const routes: RouteRecordRaw[] = [
    
    ];
    
    const router = createRouter({
      history: createWebHistory(process.env.BASE_URL),
      routes,
    });
    
    export default router;
    

安装vuex

cnpm i -D vuex@4.0.0-beta.4
  • 在src目录下,新建store文件夹

    index.ts

    import { createStore } from 'vuex';
    
    export default createStore({
      state:{},
      mutations: {},
      actions: {},
      modules: {},
    });
    

修改main.js==>main.ts

import { createApp } from 'vue';
import App from './App';
import router from './router';
import store from './store';

createApp(App).use(router).use(store).mount('#app');

修改App.vue==>App.tsx

import { defineComponent } from 'vue';
import HelloWorld from './components/HelloWorld';
import img from './assets/logo.png';

export default defineComponent({
  name: 'App',
  setup() {
    return () => (
      <div id='app'>
        <img alt='Vue logo' src={img} />
        <HelloWorld msg='Vue vite' />
      </div>
    );
  },
});

修改HelloWorld.vue==>HelloWorld.tsx

import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  setup(props) {
    const count = ref<number>(0);

    return () => (
      <div class="main">
        <h1> {props.msg}</h1>
        <button>{count.value}</button>
      </div>
    );
  },
});

运行

npm run dev

新增页面路由

view/home/index.vue

<template>
  <div class="main">
    <h1>Home</h1>
    <h2>{{ title }}</h2>
  </div>
</template>

<script lang="ts">
  import { ref } from 'vue';
  export default {
    name: 'home',
    setup() {
      const title = ref<string>('我是标题哦');
      return { title };
    },
  };
</script>

view/about/index.tsx

import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'about',
  setup() {
    const title = ref<string>('我也是标题哦');
    return () => (
      <div class='main'>
        <h1>About</h1>
        <h2>{title.value}</h2>
      </div>
    );
  },
});

路由修改

import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router';

const routes: RouteRecordRaw[] = [
  {
    path: '/home',
    name: 'home',
    component: () => import('../views/home/index.vue'),
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/about/index.tsx'),
  },
];

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes,
});

export default router;

主页面App.tsx修改

import { defineComponent } from 'vue';
import HelloWorld from './components/HelloWorld';
import img from './assets/logo.png';
import { RouterView, RouterLink } from 'vue-router';

export default defineComponent({
  name: 'App',
  setup() {
    return () => (
      <div id='app'>
         {/* 直接使用<h2>Home</h2>会warning */}
        <RouterLink to='/home'>{() => <h2>Home</h2>}</RouterLink>
        <RouterLink to='/about'>{() => <h2>About</h2>}</RouterLink>
        <img alt='Vue logo' src={img} />
        <HelloWorld msg='Vue vite' />
        <RouterView></RouterView>
      </div>
    );
  },
});

使用store

import { createStore } from 'vuex';

export default createStore({
  state: {
    title: 'store标题',
  },
  mutations: {},
  actions: {},
  modules: {},
});

App.tsx使用

import { useStore } from 'vuex';
setup() {
    const { state } = useStore();

    return () => (
      <div id='app'>
        <h3>{state.title}</h3>
      </div>
    );
 }