1. 程式人生 > 前端設計 >定製一個可以react和vue共存的ts專案

定製一個可以react和vue共存的ts專案

前言

跟風微前端,看了一圈原始碼,覺得微前端並不適合公司目前的專案,例如css會有多次載入的問題等,而且我們也不會有juqery的庫,所以將html解析成字串動態插入並執行的路子在我這並非最優解,現在花點時間做一個共用的專案

github

https://github.com/757566833/react-vue
複製程式碼

庫的結構

以react為主,vue輔助,layout也是react寫的

預覽

專案期望

支援前後端分離,自帶前端prod伺服器,方便整合websocket等

關鍵字

react vue antd webpack umd

整個專案需要的庫

解析ts用的babel而不是ts-loader/awesome-typescript-loader

// 這裡不會細說webpack 如果用開源腳手架例如umi等 需要了解下webpack內容

// 前兩個是核心 第三個是熱更新伺服器 第四個是區分webpack mode 用的merge工具,具體內容請看webpack官方文件(不要看中文版本)
yarn add webpack webpack-cli webpack-dev-server webpack-merge --dev

//  babel全家桶和react等,具體檢視babel官網,還有很多外掛可用
yarn add  @babel/core  @babel/plugin-proposal-class-properties  @babel/plugin-proposal-decorators  @babel/preset-env  @babel/preset-react  @babel/preset-typescript  --dev

// webpack外掛和loader 有些外掛不是必須安裝 例如clean可以用rm命令代替 cross-env在非多人合作下也沒什麼用 error-overlay 在此專案也會失效 自己酌情處理 在筆者寫這個文件的時候 vue-loader剛好更新到16 改版有點大
yarn add assets-webpack-plugin  babel-loader  clean-webpack-plugin  cross-env  css-loader  error-overlay-webpack-plugin  file-loader  fork-ts-checker-webpack-plugin  html-webpack-plugin  less less-loader  mini-css-extract-plugin  node-sass   sass-loader  vue-loader@15 webpack-bundle-analyzer url-loader  --dev

// react 熱更新外掛
yarn add @hot-loader/react-dom react-hot-loader --dev

// eslint 自己酌情安裝
yarn add eslint --dev

// 最主要的庫
yarn add typescript  react immutable  react-dom react-redux react-router react-router-dom styled-components  redux  vue  vue-class-component  vue-property-decorator  vue-template-compiler --dev

// ui庫 vue的省略了
yarn add antd react-resizable --dev

// prod 下伺服器的庫(koa)
yarn add koa  koa-router  koa-send  koa2-cors 

// 補一下types
yarn add @types/assets-webpack-plugin  @types/html-webpack-plugin  @types/koa  @types/koa-router  @types/koa-send  @types/koa2-cors  @types/mini-css-extract-plugin  @types/react  @types/react-dom  @types/react-hot-loader @types/react-redux  @types/react-resizable  @types/react-router-dom  @types/styled-components  @types/webpack-bundle-analyzer @types/webpack-dev-server  @types/webpack-merge   --dev
// ts 執行環境
yarn add ts-node --dev
複製程式碼

最終我的package.json

{
  "devDependencies": {
    "@babel/core": "^7.10.3","@babel/plugin-proposal-class-properties": "^7.10.1","@babel/plugin-proposal-decorators": "^7.10.3","@babel/preset-env": "^7.10.3","@babel/preset-react": "^7.10.1","@babel/preset-typescript": "^7.10.1","@hot-loader/react-dom": "^16.13.0","@types/assets-webpack-plugin"
: "^3.9.0","@types/html-webpack-plugin": "^3.2.3","@types/koa": "^2.11.3","@types/koa-router": "^7.4.1","@types/koa-send": "^4.1.2","@types/koa2-cors": "^2.0.1","@types/mini-css-extract-plugin": "^0.9.1","@types/react": "^16.9.41","@types/react-dom": "^16.9.8","@types/react-hot-loader": "^4.1.1","@types/react-redux": "^7.1.9","@types/react-resizable": "^1.7.2","@types/react-router-dom": "^5.1.5","@types/styled-components": "^5.1.0","@types/webpack-bundle-analyzer": "^3.8.0","@types/webpack-dev-server": "^3.11.0","@types/webpack-merge": "^4.1.5","@typescript-eslint/eslint-plugin": "^3.4.0","@typescript-eslint/parser": "^3.4.0","antd": "^4.3.5","assets-webpack-plugin": "^5.0.2","babel-loader": "^8.1.0","clean-webpack-plugin": "^3.0.0","cross-env": "^7.0.2","css-loader": "^3.6.0","error-overlay-webpack-plugin": "^0.4.1","eslint": "^7.3.1","eslint-config-google": "^0.14.0","eslint-plugin-react": "^7.20.0","eslint-plugin-vue": "^6.2.2","file-loader": "^6.0.0","fork-ts-checker-webpack-plugin": "^5.0.5","html-webpack-plugin": "^4.3.0","immutable": "^4.0.0-rc.12","less": "^3.11.3","less-loader": "^6.1.2","mini-css-extract-plugin": "^0.9.0","node-sass": "^4.14.1","react": "^16.13.1","react-dom": "^16.13.1","react-hot-loader": "^4.12.21","react-redux": "^7.2.0","react-resizable": "^1.10.1","react-router": "^5.2.0","react-router-dom": "^5.2.0","redux": "^4.0.5","sass-loader": "^8.0.2","styled-components": "^5.1.1","ts-node": "^8.10.2","typescript": "^3.9.5","vue": "^2.6.11","vue-class-component": "^7.2.3","vue-loader": "15","vue-property-decorator": "^9.0.0","vue-template-compiler": "^2.6.11","webpack": "^4.43.0","webpack-cli": "^3.3.12","webpack-dev-server": "^3.11.0","webpack-merge": "^4.2.2" },"dependencies": { "koa": "^2.13.0","koa-router": "^9.0.1","koa-send": "^5.0.0","koa2-cors": "^2.0.6" } } 複製程式碼

eslint

// 這個會有命令提示,自己選就好了,最後會提示你缺的依賴,最後提示我缺的依賴我沒用自動安裝,手動用yarn安裝的,因為自動安裝呼叫的npm命令。注意,eslint有一些和ts相容不是很好
npx eslint --init
// 新增官方推薦的eslint
 yarn add eslint-plugin-react-hooks  --dev
複製程式碼

最終我的eslint

我不熟vue 需要自己加

env:
  browser: true
  es2020: true
  node: true
extends:
  - "eslint:recommended"
  - "plugin:react/recommended"
  - google
parser: "@typescript-eslint/parser"
parserOptions:
  ecmaFeatures:
    jsx: true
  ecmaVersion: 11
  sourceType: module
plugins:
  - react
  - "@typescript-eslint"
  - "react-hooks"
rules:
  no-unused-vars: "off"
  no-prototype-builtins: "off"
  react-hooks/rules-of-hooks: "error"
  react-hooks/exhaustive-deps: "warn"
  react/jsx-uses-react: "error"
  react/jsx-uses-vars: "error"
  no-undef: "off"
  object-curly-spacing: ["error","always"]
  react/prop-types: 0
  max-len: ["error",{ "code": 120 }]

複製程式碼

typescript

npx typescript --init
複製程式碼

最終我的tsconfig

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Basic Options */
    // "incremental": true,/* Enable incremental compilation */
    "target": "es5",/* Specify ECMAScript target version: 'ES3' (default),'ES5','ES2015','ES2016','ES2017','ES2018','ES2019','ES2020',or 'ESNEXT'. */
    "module": "commonjs",/* Specify module code generation: 'none','commonjs','amd','system','umd','es2015','es2020',or 'ESNext'. */
    // "lib": [],/* Specify library files to be included in the compilation. */
    // "allowJs": true,/* Allow javascript files to be compiled. */
    // "checkJs": true,/* Report errors in .js files. */
    "jsx": "react",/* Specify JSX code generation: 'preserve','react-native',or 'react'. */
    // "declaration": true,/* Generates corresponding '.d.ts' file. */
    // "declarationMap": true,/* Generates a sourcemap for each corresponding '.d.ts' file. */
    "sourceMap": true,/* Generates corresponding '.map' file. */
    // "outFile": "./",/* Concatenate and emit output to single file. */
    // "outDir": "./",/* Redirect output structure to the directory. */
    // "rootDir": "./",/* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,/* Enable project compilation */
    // "tsBuildInfoFile": "./",/* Specify file to store incremental compilation information */
    // "removeComments": true,/* Do not emit comments to output. */
    // "noEmit": true,/* Do not emit outputs. */
    // "importHelpers": true,/* Import emit helpers from 'tslib'. */
    "downlevelIteration": true,/* Provide full support for iterables in 'for-of',spread,and destructuring when targeting 'ES5' or 'ES3'. */
    // "isolatedModules": true,/* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true,/* Enable all strict type-checking options. */
    // "noImplicitAny": true,/* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,/* Enable strict null checks. */
    // "strictFunctionTypes": true,/* Enable strict checking of function types. */
    // "strictBindCallApply": true,/* Enable strict 'bind','call',and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,/* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,/* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,/* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true,/* Report errors on unused locals. */
    "noUnusedParameters": true,/* Report errors on unused parameters. */
    // "noImplicitReturns": true,/* Report error when not all code paths in function return a value. */
    // "noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    // "moduleResolution": "node",/* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "./",/* Base directory to resolve non-absolute module names. */
    "paths": {
      "@/*":["./src/*"],},/* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],/* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],/* List of folders to include type definitions from. */
    // "types": [],/* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,/* Allow default imports from modules with no default export. This does not affect code emit,just typechecking. */
    "esModuleInterop": true,/* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    // "preserveSymlinks": true,/* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,/* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",/* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",/* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,/* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,/* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    // "experimentalDecorators": true,/* Enables experimental support for ES7 decorators. */
    // "emitDecoratorMetadata": true,/* Enables experimental support for emitting type metadata for decorators. */

    /* Advanced Options */
    "skipLibCheck": true,/* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true  /* Disallow inconsistently-cased references to the same file. */
  }
}

複製程式碼

程式碼組成

layout 為main專案,頁面本身為r和v專案
main 和r 用react
v用vue

專案結構

專案結構未必合理,僅僅是一個demo 根目錄下新建src

|-- src
    |-- asset      靜態檔案
    |-- global     全域性檔案例如方便moment全域性設定等
    |-- components 專案通用元件
    |-- config     專案的一些配置
    |-- http       封裝http請求,例如fetch axios等
    |-- layouts    main的具體內容
    |-- menu       layout 左側的menu
    |-- pages
        |--react   react的內容
        |--vue     vue的內容
    |--redux      實際上沒什麼用 在這裡僅僅main用到了
    |--services   各種介面
    |--util       工具
        |--react.tsx
        |--vue.ts
複製程式碼

webpack

這裡我專案和其他的不一樣 antd 不再使用按需載入,因為已有的專案使用了antd的全部元件

跟目錄下新建webpack資料夾 webpack資料夾下 新建 main react vue三個資料夾

三個資料夾下分別新建webpack.common.ts webpack.dev.ts webpack.prod.ts

main下額外新建一個 template.html

main 的common

import path from 'path';
// 生成html的外掛
import HtmlWebpackPlugin from 'html-webpack-plugin';
// 把css拆出來的外掛
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import webpack from 'webpack';

const config: webpack.Configuration = {
  entry: {
    main: './src/layouts/index.tsx',module: {
    rules: [
      {
        test: /\.(tsx|ts)?$/,exclude: /node_modules/,use: [
          {
            loader: 'babel-loader',options: {
              presets: [
                '@babel/preset-env','@babel/preset-typescript','@babel/preset-react',],plugins: [
                ['@babel/plugin-proposal-decorators',{ legacy: true }],['@babel/plugin-proposal-class-properties',{ loose: true }],{
        test: /\.css$/,use: [{
          loader: MiniCssExtractPlugin.loader,{
          loader: 'css-loader',{
        test: /\.less$/,{
          loader: 'less-loader',{
        test: /\.scss$/,{
          loader: 'sass-loader',}],{
        test: /\.(jpg|jpeg|png|svg|gif|woff|woff2|otf|ttf)?$/,loader: 'url-loader',options: {
          limit: 8192,publicPath: '/',name: 'img/[name].[hash:7].[ext]',resolve: {
    // 自動字尾
    extensions: ['.tsx','.ts'],// 軟連線
    alias: {
      '@': path.resolve('src'),plugins: [
    // 生成html
    new HtmlWebpackPlugin({
      title: 'test',template: path.resolve(__dirname,'template.html'),}),// 拆css
    new MiniCssExtractPlugin({
      filename: 'main/[name].[contenthash].css',// 檢查型別
    new ForkTsCheckerWebpackPlugin(),};
export default config;

複製程式碼

main 的dev

import path from 'path';
import webpack from 'webpack';
import merge from 'webpack-merge';
import common from './webpack.common';
// 因為是babel轉譯的ts 現在需要個外掛檢查型別
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
const config: webpack.Configuration = merge(common,{
  mode: 'development',devtool: 'eval-source-map',output: {
    path: path.resolve(__dirname,'..','dist'),filename: 'main/app.js',devServer: {
    // spa必備
    historyApiFallback: { index: '/' },contentBase: path.join(__dirname,host: '127.0.0.1',hot: true,port: 7000,// 這個的作用是讓webpack安靜點
    stats: 'errors-warnings',plugins: [
    // 熱更外掛
    new webpack.HotModuleReplacementPlugin(),// 名稱空間 也是熱更用的
    new webpack.NamedModulesPlugin(),// 全域性變數 區分環境
    new webpack.DefinePlugin({
      ENV_MODE: JSON.stringify('development'),// 熱更必備
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom',});
// 覆蓋掉common的配置,加入熱更的babel
const config2 = merge.smart(config,{
  module: {
    rules: [{
      test: /\.(tsx|ts)?$/,use: [
        {
          loader: 'babel-loader',options: {
            cacheDirectory: true,babelrc: false,presets: [
              '@babel/preset-env',plugins: [
              ['@babel/plugin-proposal-decorators','react-hot-loader/babel',});
export default config2;

複製程式碼

main 的prod

import path from 'path';
import webpack from 'webpack';
// 分析打包
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import merge from 'webpack-merge';
import common from './webpack.common';
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
const config: webpack.Configuration = merge(common,{
  mode: 'production',devtool: 'source-map',externals: {
    'react': 'React','react-dom': 'ReactDOM','react-router': 'ReactRouter','react-router-dom': 'ReactRouterDOM','antd': 'antd',output: {
    // 改成了chunk命名,避免出現0123這種
    filename: 'main/[name].[chunkhash].js',path: path.resolve(__dirname,plugins: [
    new BundleAnalyzerPlugin(),new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: ['main/**/*'],new webpack.DefinePlugin({
      ENV_MODE: JSON.stringify('production'),});

export default config;

複製程式碼

react的webpack common

// 採取約定式,pages下面的react裡面所有的tsx檔案(不包含某些關鍵字)全部為入口檔案
...
const getEntry = (url: string) => {
  if (url.includes('component') ||
    url.includes('hooks') ||
    url.includes('services') ||
    url.includes('http')
  ) {
    return;
  }
  const list = fs.readdirSync(url);
  for (const iterator of list) {
    if (iterator.includes('.tsx')) {
      entry[`/${url}/${iterator}`
          .replace('/index.tsx','')
          .replace('.tsx','')
          .replace('src/pages/react/','')
          .replace(/\//g,'')] = `./${url}/${iterator}`;
    } else if (!iterator.includes('.')) {
      getEntry(`${url}/${iterator}`);
    }
  }
};
const rootpath = path.join('src','pages','react');
...
複製程式碼

其餘的不再贅述 demo裡有

正式開始專案

1.靜態服務配置檔案

src/config/index.tsx

字尾ts tsx都可以,這個檔案是main呼叫

// react 在7001埠 vue在7002
export const cssPrefix = 'egu-layout-';
export const modulePath = {
  r: {
    development: '//127.0.0.1:7001',production: '',v: {
    development: '//127.0.0.1:7002',};

複製程式碼

2. main開始

1. src/layouts/index.tsx

// 簡單的react 入門
import React from 'react';
import ReactDOM from 'react-dom';
import {
  BrowserRouter as Router,Route,} from 'react-router-dom';
import store from '@/redux/store';
import { Provider } from 'react-redux';
import Base from './base';
import '@/global/react/global.scss';
import '@/global/react/global';
// antd 限定了中文
import { ConfigProvider } from 'antd';
import zhCN from 'antd/es/locale/zh_CN';
const Layout: React.FC = () => {
    return <Provider store={store}>
      <ConfigProvider locale={zhCN}>
         {/* base模組 */}
        <Base />
      </ConfigProvider>
    </Provider>;
};

const AppRouter: React.FC = () => {
  return (
    <Router>
      <Route path="*" component={Layout} />
    </Router>
  );
};

const App: React.FC = () => {
  return <>
    <AppRouter />
  </>;
};
ReactDOM.render(<App />,document.getElementById('root'));
複製程式碼

2. src/menu/interface.ts

資料型別不是通用的,此資料型別僅為demo

// menu的item
export interface IMenuBean {
  title: string;
  path: string;
  module?: 'r' | 'v'
  type?: EMenuType;
  authority?: string;
  children?: IMenuBean[];
}
// menu item的型別
export enum EMenuType {
  SubMenu = 'SubMenu',Item = 'Item',NoMenu = 'NoMenu',Header = 'Header'
}
// 這個模組屬於react還是vue
export enum module {
  react = 'r',vue = 'v'
}
複製程式碼

3. src/menu/index.ts

import { IMenuBean } from './interface';
import basics from './basics';
import mall from './mall';
import assets from './assets';
import finance from './finance';
import config from './config';

const menu: IMenuBean[] = [
  basics,// 運營管理
  mall,// 業務管理
  finance,// 財務
  assets,// 資產
  config,// 設定

];
export default menu;
複製程式碼

4.src/menu/basics.ts

import { IMenuBean,EMenuType,module } from './interface';
const basics: IMenuBean = {
  title: 'menu1',type: EMenuType.Header,path: '/basics/',authority: 'menu1',module: module.react,children: [
    {
      title: 'submenu1',type: EMenuType.SubMenu,path: '/basics/enterprise',authority: 'pc-op-enterprise',children: [
        {
          title: 'menu11',type: EMenuType.Item,path: '/basics/enterprise/authentication',authority: 'menu11',{
          title: 'menu12',path: '/basics/enterprise/menu',authority: 'menu12',{
      title: 'submenu2',path: '/basics/test',authority: 'pc-op-account',children: [
        {
          title: 'menu21',path: '/basics/test/test1',module: module.vue,authority: 'menu21',};
export default basics;
複製程式碼

其餘的看原始碼

  1. src/services/index.ts
// 模擬許可權
export const getWebAuthority:()=>Promise<{[key:string]:boolean}> = async ()=>{
    return {
        menu1:true,submenu1:true,menu11:true,submenu2:true,menu21:true
    }
}
複製程式碼
  1. src/layouts/base/index.tsx

複製程式碼