1.项目能力支持
1.项目初始化脚手架
1.前端编码规范工程化(lint工具、Node CLI等)
2.用工具提升项目的编码规范,如:eslint、stylelint、commitlint、markdownlint、husky等
3.工具对于JavaScript、Typescript、React、Vue等不同类型的前端项目下的标准的语法限制;
2.相关基础功能
• React : 前端页面展示框架;
• Redux :状态管理;
• React Router :前端路由;
• Connected React Router :支持将 Redux 与 React Router 进行绑定;
• Express 服务端;
• TypeScript 类型检查;
• Webpack 打包构建工具;
• Babel : ES6+ 转 ES5 工具;
• nodemon :监测 Node 应用变化时,自动重启服务器;
• axios 基于 Promise 的 HTTP 客户端;
• react-helmet :在客户端及服务端上管理标题、 meta 、样式和脚本标签;
• loadable-component :支持组件的懒加载;
• Webpack Dev Middleware :通过 Express 服务器提供 webpack 服务;
• Webpack Hot Middleware :支持基于 Express 的热更新;
• Webpack Bundle Analyzer :打包分析工具;
• morgan :服务器日志;
• terser-webpack-plugin :压缩 JS ;
• css-minimizer-webpack-plugin :压缩 CSS ;
3.运行指令
使用cross-env提供跨平台的设置及环境变量:
step1
====================================安装初始脚手架
命令行 start
sudo npm install -g encode-fe-lint
sudo encode-fe-lint init
React 项目 (TypeScript)
Y Y Y
====================================命令行 end
step2
基础环境配置
- 查看 package.json 文件
"private"
"script"-"preinstall"/"prepare"/"init"
"engines"
"devDependencies"
"dependencies"
====================================命令行 start
sudo npm install -g pnpm
sudo pnpm install
====================================命令行 end
2.项目初始化配置
项目目录:
新建 babel.config.js,以及内部配置
tsconfig 的配置, tsconfig.json
sudo npm install -g typescript
sudo tsc --init
实现相关的 postcss,创建 postcss.config.js
webpack cross-env
我们需要客户端和服务端渲染,所以如下
webpack 目录
base.config.ts
client.config.ts
server.config.ts
我们看到网上很多都是如下
webpack.common.js
webpack.dev.js
webpack.prod.js
webpack 是纯用于打包的,和 js/ts 没有关系的
webpack5 中 MiniCssExtractPlugin,将 css 分离出
progressPlugin 编译进度包
webpack-manifest-plugin ssr 中需要引入的,页面中的基本信息
loadablePlugin 分包的方式引入子包的内容
DefinePlugin 定义全局变量
bundle-analyze plugin 分析线上的包
⭐️⭐️⭐️⭐️⭐️⭐️⭐️
通用的能力做抽离,根据不同的环境,进行不同的配置。
先打包构建,生产产物
nodemon.json,开发过程中,可以监听 public 下面文件的变化&&服务端的更新
到这一步为止,基础环境已经配置完成.不用脚手架,自己手写也可以,加油吧~
3.客户端配置
1.入口文件
// src/app/index.tsx
const App = ({ route }: Route): JSX.Element => (
{config.APP.title}
{/ Child routes won't render without this /}
{renderRoutes(route.routes)}
};
// src/client/index.tsx
const render = (Routes: RouteConfig[]) =>
ReactDOM.hydrate(
{renderRoutes(Routes)}
,
document.getElementById('react-view'),
);
// loadable-component setup
loadableReady(() => render(routes as RouteConfig[]));
2.错误边界处理
import { ReactNode, PureComponent } from "react";
interface Props {
children?: ReactNode;
}
interface State {
error: Error | null;
errorInfo: { componentStack: string } | null;
}
class ErrorBoundary extends PureComponent {
constructor(props: Props) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch(error: Error, errorInfo: { componentStack: string }): void
// Catch errors in any components below and re-render with error message
this.setState({ error, errorInfo });
// You can also log error messages to an error reporting service here
}
render(): ReactNode {
const { children } = this.props;
const { errorInfo, error } = this.state;
// If there's an error, render error path
return errorInfo ? (
Something went wrong.
{error && error.toString()}
{errorInfo.componentStack}
) : (
children || null
);
}
}
export default ErrorBoundary;
3.页面入口配置
const Home: FC = (): JSX.Element => {
const dispatch = useDispatch();
const { readyStatus, items } = useSelector(
({ userList }: AppState) => userList,
shallowEqual
);
// Fetch client-side data here
useEffect(() => {
dispatch(fetchUserListIfNeed());
}, [dispatch]);
const renderList = () => {
if (!readyStatus || readyStatus === "invalid" || readyStatus === "request"
return
Loading...
;if (readyStatus === "failure") return
Oops, Failed to load list!
;return ;
};
return (
{renderList()}
);
};
// Fetch server-side data here
export const loadData = (): AppThunk[] => [
fetchUserListIfNeed(),
// More pre-fetched actions...
];
export default memo(Home);
1
4.路由配置
export default [
{
component: App,
routes: [
{
path: "/",
exact: true,
component: AsyncHome, // Add your page here
loadData: loadHomeData, // Add your pre-fetch method here
},
{
path: "/UserInfo/:id",
component: AsyncUserInfo,
loadData: loadUserInfoData,
},
{
component: NotFound,
},
],
},
] as RouteConfig[];
4.服务端配置
1.请求配置
// 使用https://jsonplaceholder.typicode.com提供的接口设置请求
export default {
HOST: 'localhost',
PORT: 3000,
API_URL: 'https://jsonplaceholder.typicode.com',
APP: {
htmlAttributes: { lang: 'zh' },
title: '萍宝贝 ES6 项目实战',
titleTemplate: '萍宝贝 ES6 项目实战 - %s',
meta: [
{
name: 'description',
content: 'wikiHong ES6 React 项目模板',
},
],
},
};
2.入口文件
const app = express();
// Use helmet to secure Express with various HTTP headers
app.use(helmet({ contentSecurityPolicy: false }));
// Prevent HTTP parameter pollution
app.use(hpp());
// Compress all requests
app.use(compression());
// Use for http request debug (show errors only)
app.use(logger('dev', { skip: (_, res) => res.statusCode < 400 }));
app.use(favicon(path.resolve(process.cwd(), 'public/logo.png')));
app.use(express.static(path.resolve(process.cwd(), 'public')));
// Enable dev-server in development
if (DEV) devServer(app);
// Use React server-side rendering middleware
app.get('*', ssr);
// @ts-expect-error
app.listen(config.PORT, config.HOST, (error) => {
if (error) console.error(chalk.red(==> 😭 OMG!!! ${error}
));
});
3.html渲染
const html = <!doctype html> <html ${head.htmlAttributes.toString()}> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000" /> <link rel="icon" href="/logo.png" /> <link rel="apple-touch-icon" href="/logo192.png" /> <link rel="manifest" href="/manifest.json" /> ${head.title.toString()} ${head.base.toString()} ${head.meta.toString()} ${head.link.toString()} <!-- Insert bundled styles into <link> tag --> ${extractor.getLinkTags()} ${extractor.getStyleTags()} </head> <body> <!-- Insert the router, which passed from server-side --> <div id="react-view">${htmlContent}</div> <!-- Store the initial state into window --> <script> // Use serialize-javascript for mitigating XSS attacks. See the foll // http://redux.js.org/docs/recipes/ServerRendering.html#security-co window.__INITIAL_STATE__=${serialize(initialState)}; </script> <!-- Insert bundled scripts into <script> tag --> ${extractor.getScriptTags()} ${head.script.toString()} </body> </html>
;
const minifyConfig = {
collapseWhitespace: true,
removeComments: true,
trimCustomFragments: true,
minifyCSS: true,
minifyJS: true,
minifyURLs: true,
};
// Minify HTML in production
return DEV ? html : minify(html, minifyConfig);
4.本地服务配置
export default (app: Express): void => {
const webpack = require("webpack");
const webpackConfig = require("../../webpack/client.config").default;
const compiler = webpack(webpackConfig);
const instance = require("webpack-dev-middleware")(compiler, {
headers: { "Access-Control-Allow-Origin": "" },
serverSideRender: true,
});
app.use(instance);
app.use(
require("webpack-hot-middleware")(compiler, {
log: false,
path: "/__webpack_hmr",
heartbeat: 10 1000,
})
);
instance.waitUntilValid(() => {
const url = http://${config.HOST}:${config.PORT}
;
console.info(chalk.green(==> 🌎 Listening at ${url}
));
});
};
5.SSR配置
export default async (
req: Request,
res: Response,
next: NextFunction
): Promise => {
const { store } = createStore({ url: req.url });
// The method for loading data from server-side
const loadBranchData = (): Promise => {
const branch = matchRoutes(routes, req.path);
const promises = branch.map(({ route, match }) => {
if (route.loadData)
return Promise.all(
route
.loadData({
params: match.params,
getState: store.getState,
req,
res,
})
.map((item: Action) => store.dispatch(item))
);
return Promise.resolve(null);
});
return Promise.all(promises);
};
try {
// Load data from server-side first
await loadBranchData();
const statsFile = path.resolve(process.cwd(), "public/loadable-stats");
const extractor = new ChunkExtractor({ statsFile });
const staticContext: Record = {};
const App = extractor.collectChunks(
{/ Setup React-Router server-side rendering /}
{renderRoutes(routes)}
);
const initialState = store.getState();
const htmlContent = renderToString(App);
// head must be placed after "renderToString"
// see: https://github.com/nfl/react-helmet#server-usage
const head = Helmet.renderStatic();
// Check if the render result contains a redirect, if so we need to set
// the specific status and redirect header and end the response
if (staticContext.url) {
res.status(301).setHeader("Location", staticContext.url);
res.end();
return;
}
// Pass the route and initial state into html template, the "statusCode" c
res
.status(staticContext.statusCode === "404" ? 404 : 200)
.send(renderHtml(head, extractor, htmlContent, initialState));
} catch (error) {
res.status(404).send("Not Found :(");
console.error(chalk.red(==> 😭 Rendering routes error: ${error}
));
}
next();
};
5.构建工具处理
1.基础配置
const config = (isWeb: boolean):Configuration =>({
mode: isDev ? 'development' : 'production',
context: path.resolve(process.cwd()), // 上下文中的传递
// 压缩大小, 性能优化
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true // 保留console的内容
}
}
})
]
},
plugins: getPlugins(isWeb) as WebpackPluginInstance[],
module: {
// 解析对应的loader
rules: [
{
test: /.(t|j)sx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
caller: { target: isWeb ? 'web' : 'node' },
cacheDirectory: isDev,
},
},
{
test: /.css$/,
use: getStyleLoaders(isWeb),
},
{
test: /.(scss|sass)$/,
use: getStyleLoaders(isWeb, true),
},
{
test: /.(woff2?|eot|ttf|otf)$/i,
type: 'asset',
generator: { emit: isWeb },
},
{
test: /.(png|svg|jpe?g|gif)$/i,
type: 'asset',
generator: { emit: isWeb },
},
]
}
})
export default config;
getStyleLoaders 配置
// loader style-loader postcss-loader
const getStyleLoaders = (isWeb: boolean, isSaas?: boolean) => {
let loaders: RuleSetUseItem[] = [
{
loader: 'css-loader',
options: {
importLoaders: isSaas ? 2 : 1,
modules: {
auto: true,
exportOnlyLocals: !isWeb, // ssr
}
}
},
{
loader: 'postcss-loader',
}
];
if (isWeb) {
loaders = [
...loaders,
]
}
if (isSaas)
loaders = [
...loaders,
{
loader: 'sass-loader',
}
];
return loaders
}
getPlugins 配置
// plugins csr ssr
const getPlugins = (isWeb: boolean) => {
let plugins = [
new webpack.ProgressPlugin(),
// 适用于SSR服务下的manifest信息
new WebpackManifestPlugin({}), // 改变ssr返回页面的title
new LoadablePlugin({
writeToDisk: true,
filename: '../loadable-state.json', // 声明写入文件中的名称
}),
// 定义全局变量
new webpack.DefinePlugin({
CLIENT: isWeb,
SERVER: !isWeb,
DEV: isDev,
})
];
// 根据process,env NODE_ENV analyze
if (!isDev) {
plugins = [
...plugins,
new BundleAnalyzerPlugin({
analyzerMode: process.env.NODE_ENV === 'analyze' ? 'server' : 'disabled'
}),
];
}
return plugins
}
2.客户端配置
const config:Configuration = {
devtool: isDev && 'eval-cheap-source-map',
entry: './src/client',
output: {
filename: isDev ? '[name].js' : '[name].[contenthash].js',
chunkFilename: isDev ? '[id].js' : '[id].[contenthash].js',
path: path.resolve(process.cwd(), 'public/assets'),
publicPath: '/assets/',
},
optimization: {
minimizer: [new CssMinimizerPlugin()]
},
plugins: getPlugins()
}
export default merge(baseConfig(true), config)
getPlugins 配置
const getPlugins = () => {
let plugins = []
if (isDev) {
plugins = [
...plugins,
// 热更新
new webpack.HotModuleReplacementPlugin(),
// react refresh
new ReactRefreshWebpackPlugin(),
]
}
return plugins;
}
3.服务器端配置
const config: Configuration = {
target: 'node',
devtool: isDev ? 'inline-source-map' : 'source-map',
entry: './src/server',
output: {
filename: 'index.js',
chunkFilename: '[id].js',
path: path.resolve(process.cwd(), 'public/server'),
libraryTarget: 'commonjs2',
},
node: { dirname: true, filename: true },
externals: [
'@loadable/component',
nodeExternals({
// Load non-javascript files with extensions, presumably via loaders
allowlist: [/.(?!(?:jsx?|json)$).{1,5}$/i],
}),
] as Configuration['externals'],
plugins: [
// Adding source map support to node.js (for stack traces)
new webpack.BannerPlugin({
banner: 'require("source-map-support").install();', // 最新的更新时间
raw: true,
}),
],
};