吐槽

公司自己的产品,由于历史遗留问题,前端一直是和java放到一个项目里写的。

导致了,前端就被死死的绑在了IDEA战车上。想要看页面效果,先起几个java服务。想要调试一个改动,重启个java服务先。(热更新是有,但是间歇性失效,会给调试带来意想不到的困扰。)

选择 React.js 的原因

打算做前后分离,之前的技术路线是 Vue.js 多页。想多掌握些技能,对现有产品的结构,进行一次改革,解放前端。(产品的代码量已经不小了)于是咨询大佬,在方少和各位 React.js 大佬的力荐下。大胆的尝试使用 React.js (以前虽然接触过,但没写过)。如果只是实现逻辑,什么框架都可以。写过之后,React.js 的那种掌控感,可实现性,个人很喜欢,虽然也遇到很多坑。说这些,希望能给遇到类似问题的开发者一点参考。

正文

主要记录一下,从不会到折腾出一些东西的过程。写做分享,也写给自己。

:文中涵盖的内容,可能不是有多难,也可能存在一些不正确性。

前期

项目结构

一个适合的项目结构,会给开发带来极大的快感。

> 代表文件夹

    ./src
>assets // 静态资源
>font-awesome
logo.png
>components // 放置 dumb 组件
>alert
>icon // 将 dumb 组件需要的 icon 放到一起,方便管理
alert.js
alert.less // css 模块化,只针对 alert.js 不会造成命名污染
...
>containers // 放置 smart 组件
>http
>api // 针对不同模块的 api ,进行编写,方便多人开发,互不干扰
http.js // 对 axios 的统一配置
>utils
skin.js // 皮肤文件
App.js
index.js
registerServiceWork.js
router.js // 将路由抽离,方便管理和修改
config-overrides.js // webpack 配置
package.json

package.json


...
"dependencies": {
"ajv": "^6.5.2",
"axios": "^0.18.0",
"base64-js": "^1.3.0",
"crypto-js": "^3.1.9-1",
"file-loader": "^1.1.11",
"prop-types": "^15.6.2",
"react": "^16.4.2",
"react-app-rewire-less": "^2.1.2",
"react-dom": "^16.4.2",
"react-modal": "^3.5.1",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4",
"react-app-rewired": "^1.5.2"
},
...
  • 为什么没有 redux react-redux ? 放在后面聊,其中也是有些取舍,有些爱恨情仇。
  • 代码检测?当然 eslint 个人习惯用这个

webpack 配置

项目开始,使用 create-react-app,让人意想不到的是,项目开始的困难,并不是来自 React.js 的编写,而是 webpack 4.0create-react-app 中的配置,是隐藏起来的。npm eject 命令可以把配置暴露出来,进行配置。最后发现 react-app-rewired ,让我比较优雅的完成了配置。

react-app-rewired 的配置全都写在 config-overrides.js 中,放在项目根下,与 ./src 同级,下面是我的配置,及部分解释。

config-overrides.js

const path = require('path')
const rewireLess = require('react-app-rewire-less'); /**
* @author Itroad
* @version 0.1.0
*
* Cover webpack's configure
* @param { object } config webpack export.module = {...}
* @param { string } env production || development
* @return { object } custom config
*/
module.exports = function override(config, env) {
if (env === "production") { // File path of build
// 解决 打包后 文件引用路径问题
// 也可以在 package.json homepage: '.',配置 但我喜欢放在一起,方便管理
config.output.publicPath = '.' + config.output.publicPath // For require source file outside of src/. ( remove ModuleScopePlugin )
// config.resolve.plugins = [] // For css module
config.module.rules[1].oneOf[2].loader[2].options['modules'] = true
config.module.rules[1].oneOf[2].loader[2].options['localIdentName'] = '[name]_[local]__[hash:base64:5]' // 配置css 内对于 font 字体文件的引用路径
config.module.rules[1].oneOf[3].options['publicPath'] = '../../' // Path alias
config.resolve.alias = Object.assign({}, config.resolve.alias, {
"@src": path.resolve("src/"),
"@http": path.resolve("src/http"),
"@assets": path.resolve("src/assets"),
"@components": path.resolve("src/components"),
"@containers": path.resolve("src/containers"),
"@reducers": path.resolve("src/reducers"),
"@styles": path.resolve("src/styles"),
"@utils": path.resolve("src/utils"),
"@static": path.join(process.cwd(), './static') // 引用./src 外部资源,默认只在./src 内,本文并未使用,这里只做类举,不做推荐
}) } else { // For require source file outside of src/. ( remove ModuleScopePlugin )
// config.resolve.plugins = [] // For css module
config.module.rules[1].oneOf[2].use[1].options['modules'] = true
config.module.rules[1].oneOf[2].use[1].options['localIdentName'] = '[name]_[local]__[hash:base64:5]'
// config.module.rules[1].oneOf[2].exclude = [
// path.resolve(__dirname, 'node_modules'),
// path.resolve(__dirname, 'src/components'),
// ]
config.module.rules[1].oneOf.push({
test: /\.css$/,
use: ['style-loader', 'css-loader'],
include: [
path.resolve(__dirname, 'node_modules'),
path.resolve(__dirname, 'src/components'),
]
}) // Path alias
config.resolve.alias = Object.assign({}, config.resolve.alias, {
"@src": path.resolve("src/"),
"@http": path.resolve("src/http"),
"@assets": path.resolve("src/assets"),
"@components": path.resolve("src/components"),
"@containers": path.resolve("src/containers"),
"@reducers": path.resolve("src/reducers"),
"@styles": path.resolve("src/styles"),
"@utils": path.resolve("src/utils"),
"@static": path.join(process.cwd(), './static')
})
} // To support less
config = rewireLess(config, env); return config;
}
  • 对打包后,index.html 中的文件路径,以及 .css 文件中对外部资源引用的路径
  • css 模块化,这样命名就不是头疼(之后会具体说明)
  • 路径别名,避免路径写错,看着也优雅。缺点就是,vs code 的路径自动匹配不能用了

中期

项目结构,配置都搞定了。开始进入代码的编写。

毕竟这不是教程,那就说一些,我认为有点价值的。

由于没有采用任何的 UI 框架,所以都要自己实现

Modal

import React from 'react'
import ReactDom from 'react-dom'
import font from '@assets/font-awesome/css/font-awesome.min.css'
import style from './modal.less' const createModal = (Component, imgSrc, ModalStyle) => {
let body = document.body;
let showDom = document.createElement("div");
// 设置基本属性
showDom.classList.add(style.toast) body.appendChild(showDom); // 自我删除的方法
let close = () => {
ReactDom.unmountComponentAtNode(showDom);
body.removeChild(showDom);
} if(!ModalStyle) {
ModalStyle = {
width: '400px',
height: '500px'
}
}
if(ModalStyle) {
if(parseInt(ModalStyle.width, 10)) {
ModalStyle.width = parseInt(ModalStyle.width, 10) > (window.innerWidth - 100) ? (window.innerWidth - 100) : ModalStyle.width
} else {
ModalStyle.width = '400px'
console.error('createToast width 属性值输入错误, 已使用默认值')
} if(parseInt(ModalStyle.height, 10)) {
ModalStyle.height = parseInt(ModalStyle.height, 10) > (window.innerHeight - 100) ? (window.innerHeight - 100) : ModalStyle.height
} else {
ModalStyle.height = '500px'
console.error('createToast height 属性值输入错误, 已使用默认值')
}
} ReactDom.render(
<div className='ReactModal__Content' style={ModalStyle}>
<div className={style.head + ' skin-modal-head'}>
<div>
<img src={imgSrc} alt="emp"/>
<p>弹框标题</p>
</div>
<i className={font['fa'] + ' ' + font['fa-close']} onClick={close}></i>
</div>
<Component close={close}/>
</div>,
showDom
);
} export default createModal
  • 也许这是函数式编程吧
  • 自己创建节点,灵活植入,用完删除
  • 尺寸做了默认的设置,和根据网页可视范围宽高,对超出范围进行的处理
  • 通过 props 将弹窗关闭方法传入子组件
  • 还有一点,说不上多好的告警日志提示
  • 官网也有 createPortal() 可供使用,我是写完之后才知道,也就不改了
  • parseInt(value, 10) 防止 '' ' ' 和 不可转为数字的值

Alert

类似的方法,实现了 Alert ,其中包括:createConfirm createInfo createWarning createErrorclearAlert 清除 Alert 的方法。说下 createConfirm 其他的没什么。

const createConfirm = (msg, cb) => {
showDom = document.createElement("div"); // 设置基本属性
showDom.classList.add(style.toast)
document.body.appendChild(showDom); // 自我删除的方法
let close = () => {
ReactDom.unmountComponentAtNode(showDom);
document.body.removeChild(showDom);
} const ModalStyle = {
width: '300px',
height: '165px'
} ReactDom.render(
<div className='ReactModal__Content' style={ModalStyle}>
<div className={style.head + ' skin-modal-head'}>
<div>
<img src={confirm} alt="emp"/>
<p>确认</p>
</div>
<i className={font['fa'] + ' ' + font['fa-close']} onClick={close}></i>
</div>
<div className={style.confirmContent}>
<p className={style.msg}>
{msg}
</p>
</div>
<div className={style.footer}>
<div onClick={close}>取消</div>
<div onClick={cb}>确定</div>
</div>
</div>,
showDom
);
}

调用示例


/**
* confirm 点击确认按钮的回调
* @param {any} params
*/
confirmCallBack (params) {
console.log('test', params)
clearAlert()
} /**
* 测试 confirm 弹窗
*/
showNewConfirm () {
createConfirm('确定要删除xxx ?', this.confirmCallBack.bind(this, 123))
}
  • 用了一个 cb 回调,来处理 confirm 点击确定的事件

Toast

这个就更简单了

import React from 'react'
import ReactDom from 'react-dom'
import style from './toast.less' const createToast = (text, time) => {
let body = document.body;
let showDom = document.createElement("div");
// 设置基本属性
showDom.classList.add(style.toast) body.appendChild(showDom); // 自我删除的方法
let close = () => {
ReactDom.unmountComponentAtNode(showDom);
body.removeChild(showDom);
}
if(!parseInt(time, 10)) {
time = 1500
}
setTimeout(close, time)
ReactDom.render(
<div className={style.content}>
{text}
</div>,
showDom
);
} export default createToast
  • 第二个参数,传入关闭时间,默认 1500 毫秒

皮肤

这个就是,将皮肤样式,添加在页面的 <style></style>

/**
* @author Jiang yang
*
* @description 生成皮肤样式
* @version 0.0.1
*/ const skin = {} skin.iceBlue = {
// 全局字体颜色
appColor: '#FFFFFF',
appBgColor: 'black', // header
headerBgColor: '#010a1c', // 框架头部背景色 // left menu
leftMenuBgColor: '#2c3e50', // 左侧菜单背景色
leftMenuBorderColor: '#2c3e50', // 左侧菜单边颜色 // right menu
rightMenuBgColor: '#2c3e50', // 右侧菜单背景色
rightMenuBorderColor: '#2c3e50', // 右侧菜单边颜色 // content
contentBgColor: 'rgb(60, 71, 84)', // 框架内容部分背景色 // footer
footerBgColor: '#2c3e50', // 框架底部背景色
footerShadowColor: 'black', // 框架底部阴影色 // modal
modalOverlay: 'rgba(49, 52, 70, 0.75)', // 弹窗遮罩层
modalContentBg: '#1f2c3a', // 弹窗背景
modalContentShadow: 'gray', // 弹窗阴影
modalContentTxt: 'white', // 弹窗字体颜色
modalHeadBg: '#091323' // 弹窗头部
} skin.lightBlue = {
// 全局字体颜色
appColor: 'black',
appBgColor: 'white', // header
headerBgColor: 'blue', // footer
footerBgColor: 'blue',
footerShadowColor: 'black', // left menu
leftMenuBgColor: 'white',
leftMenuBorderColor: '#2c3e50', // right menu
rightMenuBgColor: 'white',
rightMenuBorderColor: '#2c3e50', // content
contentBgColor: 'white',
} let getSkinStyle = (skin) => {
if(!skin) {
return '';
}
return `
.skin-app {
color: ${skin.appColor};
background-color: ${skin.appBgColor};
}
.skin-header {
background-color: ${skin.headerBgColor};
}
.skin-left-menu {
background-color: ${skin.leftMenuBgColor};
border-right: 1px solid ${skin.leftMenuBorderColor};
}
.skin-right-menu {
background-color: ${skin.rightMenuBgColor};
border-left: 1px solid ${skin.rightMenuBorderColor};
}
.skin-content {
background-color: ${skin.contentBgColor};
}
.skin-footer {
background-color: ${skin.footerBgColor};
box-shadow: 0 -1px 10px ${skin.footerShadowColor};
}
.ReactModal__Overlay {
background-color: ${skin.modalOverlay} !important;
}
.ReactModal__Content {
background-color: ${skin.modalContentBg} !important;
box-shadow: 0px 0px 10px ${skin.modalContentShadow};
color: ${skin.modalContentTxt};
}
.skin-modal-head {
background-color: ${skin.modalHeadBg};
}
`
} let setSkinStyle = (skin) => {
let styleText = getSkinStyle(skin);
let oldStyle = document.getElementById('skin');
const style = document.createElement('style');
style.id = 'skin';
style.type = 'text/css';
style.innerHTML = styleText;
oldStyle ? document.head.replaceChild(style, oldStyle) : document.head.appendChild(style);
} setSkinStyle(skin.iceBlue) export {skin, setSkinStyle}

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Route } from 'react-router-dom'
import App from './App';
import Login from './components/login/login.js'
import registerServiceWorker from './registerServiceWorker'; import '@utils/skin' // 这里引入即可 ReactDOM.render(
<HashRouter>
<div style={{height:'100%'}}>
<Route exact path='/' component={Login} />
<Route path='/home' component={App} />
</div>
</HashRouter>,
document.getElementById('root')
);
registerServiceWorker();
  • 定义皮肤样色对象,然后根据对象生成样式,然后插入到页面中
  • 多个皮肤,就多个皮肤颜色对象

Menu

其实这本来也不是什么有价值的东西。但是用 React 实现的,对于没做过类似的,算是个参考吧。

menu.js

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-router-dom'
import font from '@assets/font-awesome/css/font-awesome.min.css'
import style from './menu.less' class Menu extends Component {
static propTypes = {
data: PropTypes.array
} constructor () {
super()
this.state = {
isShow: null
} } /**
* 生命周期钩子: 根据 props 的变化,更新 state
* @param {object} nextProps
*/
static getDerivedStateFromProps (nextProps, prevState) {
if (prevState.isShow) {
return null
} const data = nextProps.data /**
* 递归生成 isShow 对象,控制菜单的展开和收缩
* @param {array | object} data
* @return {item+id: true, item2: false, ...}
*/
function getIsShow (data) {
let isShow = {}
function getIsShowState (data) {
if(data instanceof Array) {
for(let item of data){
getIsShowState(item)
}
} else {
isShow['item' + data.id] = data.show
getIsShowState(data.children)
}
}
getIsShowState(data)
return isShow
} return {
isShow: data ? getIsShow(data) : null
} } /**
* 通过 id 来查找 this.state.isShow 中的数据,从而控制菜单的显示状态
* @param {number} id 菜单 id
*/
handleClickMenu (id) {
let current = {
['item'+ id]: !this.state.isShow['item'+ id]
}
let copyState = this.state.isShow
this.setState({
isShow: Object.assign({}, copyState, current)
})
} handleDisposeOperate (value) {
if(this.props.operateCallBack) {
this.props.operateCallBack(value)
}
} /**
* 递归生成菜单的 DOM 结构
* @param {array} data 菜单数据
* @param {number} id 菜单id
*/
handleCreateMenu (data, id) {
let menuDom = []; if (data instanceof Array) {
let list = []
for (let item of data) {
list.push(this.handleCreateMenu(item))
}
menuDom.push(
<ul key={id ? 'ul-' + id : 'root'} className={style.menuUl} style={{display: id ? this.state.isShow['item' + id] ? 'block' : 'none' : 'block'}}>
{list}
</ul>
)
} else {
let levelClass = data.level === 1 ? 'levelTop' : 'levelItem'
let margLeft = (data.level * 16) + 'px' menuDom.push(
<li key={data.id} id={data.id}>
{
data.children.length > 0
? <div onClick={this.handleClickMenu.bind(this, data.id)} className={style[levelClass]} style={{'paddingLeft': margLeft}}>
<div>
{
data.level === 1
? <img className={style.icon} src={require('./icon/' + data.icon)} alt="icon"/>
: ''
}
<span>{data.name}</span>
</div>
{
this.state.isShow['item' + data.id]
? <i className={font['fa'] + ' ' + font['fa-angle-down']}></i>
: <i className={font['fa'] + ' ' + font['fa-angle-right']}></i>
}
</div>
:
data.operate
? <div onClick={this.handleDisposeOperate.bind(this, data.operate)}>
<div className={style[levelClass]} style={{'paddingLeft': margLeft}}>
<div>
{
data.level === 1
? <img className={style.icon} src={require('./icon/' + data.icon)} alt="icon"/>
: ''
}
<span>{data.name}</span>
</div>
</div>
</div>
: <Link to={data.path}>
<div className={style[levelClass]} style={{'paddingLeft': margLeft}}>
<div>
{
data.level === 1
? <img className={style.icon} src={require('./icon/' + data.icon)} alt="icon"/>
: ''
}
<span>{data.name}</span>
</div>
</div>
</Link>
} {this.handleCreateMenu(data.children, data.id)} </li>
)
} return menuDom;
} render () {
return (
<div className=''>
{this.props.data ? this.handleCreateMenu(this.props.data) : ''}
</div>
)
}
} export default Menu
  • handleCreateMenu() 根据数据,生成菜单结构
  • 关于这种结构,官网也提及,要有 key 这就是为什么传入了 id,同时也是为了控制菜单折叠
  • 菜单的折叠,用 state 来控制,具体看 getIsShow()
  • 这里面我用了些 三元判断 甚至还嵌套了下

部分生命周期将要废弃

虽说这个要看官网,但是在看 React 小书 的时候,也因为生命周期的问题,耽搁了一下。

  • 首先提供一下官网说明,中文的
  • componentWillMount() 这个就不用了,挂载之前的逻辑,写到 constructor()
  • componentWillUpdate() componentWillReceiveProps() 这两个不用了,写到 static getDerivedStateFromProps()

后期

发现前面该开发的,都写差不多。后期也没什么了。

  • 我写了两份文档,DOCS.mdSTANDARD.md

    DOCS.md 是关于相关插件,函数的使用和说明

    STANDARD.md 是这个项目的代码规范文档,部分针对这个项目,多数都是普遍的规范

  • 要求打包之后,还要灵活的配置 接口公共路径

    public 中添加一个文件,env.js ,然后在 index.html 中手动引入

    env.js

    window.PROJECT_CONFIG = {
    production: "http://..."
    }

    就是把变量放到 window 下,这样代码里就能 window.PROJECT_CONFIG.production 取到想要的数据,打包后,这个文件也一样存在,可以更改。环境变更的时候,就不用改代码,然后再打包了。

为何没有使用 Redux ?

这是部分页面



图中两个组件,是有频繁交互的。当时是考虑用 reduxreact-redux 来写。而我也实现了。 但是后来思前想后,觉得这种方式,如果让组里其他前端开发的话,不是那么友好。会写很多 map...

于是,我把右侧菜单,嵌套进了中间的组件中,外面看着没变化。但通信方式,已经变成了父子组件之间的通信方式。这就比较简单了。

这种处理方式,我认为是,用结构,来取代复杂的逻辑。

是的,每个页面,如何需要,都要带有自己的右侧菜单,仁者见仁智者见智吧。

特别感谢

这个一定要说在前面,在学习 React.js 的开始,就有小伙伴推荐了 react 小书, 作者胡子大哈。这本书称得上是

良心巨制 。(截至目前,看了四遍)

React.js 三周 -- 入门到搭建团队基础项目的更多相关文章

  1. 谈谈 React.js 的核心入门知识

    近来React.js变得越来越流行,本文就来谈一谈React.js的入门实践,通过分析一些常用的概念,以及提供一些入门 的最佳编程编程方式,仅供参考. 首先需要搞懂的是,React并不是一个框架,Re ...

  2. React.js 官网入门教程 分离文件 操作无法正常显示HelloWord

    对着React官网的教程练习操作,在做到分离文件练习时,按照官网步骤来却怎么也无法正常显示HelloWord. 经测试,html文件中内容改为: <!DOCTYPE html><ht ...

  3. React第三次入门

    传统HTML开发在处理越来越多的服务器数据和用户交互数据反应到复杂界面的时候,代码量越来越大,难以维护. Angular是基于MVVM的开发框架,重量级..不适用于移动端的web栈, 其UI组件的封装 ...

  4. Node.js 蚕食计划(五)—— Koa 基础项目搭建

    Koa 是由 Express 原班人马打造的超轻量服务端框架 与 Express 相比,除了自由度更高,可以自行引入中间件之外,更重要的是使用了 ES6 + async,从而避免了回调地狱 不过也是因 ...

  5. 第三周博客之一---Oracle基础知识

    一.数据库的定义.作用介绍 1.定义:按照数据结构来组织.存储和管理数据的建立在计算机存储设备上的仓库. 2.数据库的发展历史: 2.1.在1962年数据库一词出现在系统研发的公司的技术备忘录中 2. ...

  6. 大二暑假第三周总结--开始学习Hadoop基础(二)

    简单学习NoSQL数据库理论知识 NoSQL数据库具有以下几个特点: 1.灵活的可扩展性(支持在多个节点上进行水平扩张) 2.灵活的数据模型(与关系数据库中严格的关系模型相反,显得较为松散) 3.与与 ...

  7. maven初始搭建一个基础项目(spring mvc+spring+jdbc mysql+jstl)

    技术选型: 一.项目搭建: 1)创建maven项目 (我博客里面有介绍) 选择aptach的maven-archetype-webapp 填入groupIDhe artifactId等 确认项目名称 ...

  8. springboot2.0入门(二)-- 基础项目构建+插件的使用

    一.idea中新建第一个HelloWorld项目 点击next: 下一步 在这里可以选择我们需要依赖的第三方软件类库,包括spring-boot-web,mysql驱动,mybatis等.我们这里暂时 ...

  9. Spring Boot 项目学习 (三) Spring Boot + Redis 搭建

    0 引言 本文主要介绍 Spring Boot 中 Redis 的配置和基本使用. 1 配置 Redis 1. 修改pom.xml,添加Redis依赖 <!-- Spring Boot Redi ...

随机推荐

  1. Controller层aop

    利用@Around通知修改Controller的返回值 自定义一个注解@OperationBtn 在切入点Controller上加上自定义注解 接下来就是重点了,AspectJ写切面类,对该Contr ...

  2. postgres on linux red hat 7 配置问题

    记录几个重点的东西吧,具体的步骤 不上了,网上有很多, 1.  redhat subscripiton需要订阅,没钱的需要去更换yum 源,也很简单,就是把自带的yum给删掉,重装其他的,大家可以百度 ...

  3. Install weblogic in silent mode

    使用静默(silent)模式来安装weblogic,在需要将安装脚本化,或无法使用图形界面的时候非常有用. 下面按照自己在实际工作中碰到的例子,来慢慢总结不同版本和平台weblogic的静默安装方法. ...

  4. python 关于文件的操作

    1.打开文件: f=open(r'E:\PythonProjects\test7\a.txt',mode='rt',encoding='utf-8') 以上三个单引号内分别表示:要打开的文件的路径,m ...

  5. <转> plsql dev中Dynamic Performance Tables not accessible分析解决

    相信很多使用plsql dev的朋友多遇到过类此如下面的提示: Dynamic Performance Tables not accessible, Automatic Statistics Disa ...

  6. C++ 迭代器的使用和操作

    迭代器是一种检查容器内元素并遍历元素的数据类型.C++更趋向于使用迭代器而不是下标操作,因为标准库为每一种标准容器(如vector)定义了一种迭代器类型,而只用少数容器(如vector)支持下标操作访 ...

  7. electron-vue 淘宝源

    yarn-js config set registry https://registry.npm.taobao.org

  8. object references an unsaved transient instance save the transient instance before flushing

    object references an unsaved transient instance save the transient instance before flushing 对象引用未保存的 ...

  9. HTMLParser和BeautifulSoup使用入门和总结

    1.HTMLParser一般这么用: from html.parser import HTMLParser from urllib import request class MyHtmlParser( ...

  10. PCIe 驱动流程(LTSSM)

     本次的工作是完成刚流片的FPGA中PCIe IP核的bring up,也就是芯片的中PCIe的第一个使用者,将PCIe IP核正常使用起来,并配合公司的EDA团队,完成PCIe IP核到用户的呈现. ...