定製一個可以react和vue共存的ts專案
阿新 • • 發佈:2020-06-29
前言
跟風微前端,看了一圈原始碼,覺得微前端並不適合公司目前的專案,例如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;
複製程式碼
其餘的看原始碼
- src/services/index.ts
// 模擬許可權
export const getWebAuthority:()=>Promise<{[key:string]:boolean}> = async ()=>{
return {
menu1:true,submenu1:true,menu11:true,submenu2:true,menu21:true
}
}
複製程式碼
- src/layouts/base/index.tsx
複製程式碼