1.项目能力支持
1.项目初始化脚手架
1.前端编码规范工程化(lint工具、Node CLI等)
2.用工具提升项目的编码规范,如:eslint
、stylelint
、commitlint
、markdownlint
、husky等
3.工具对于JavaScript
、Typescript
、React
、Vue
等不同类型的前端项目下的标准的语法限制;
2.相关基础功能
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.项目初始化配置
项目目录:
1. 新建 babel.config.js,以及内部配置
2. tsconfig 的配置, tsconfig.json
sudo npm install -g typescript
sudo tsc --init
3. 实现相关的 postcss,创建 postcss.config.js
4. 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 => (<div className={styles.App}><Helmet {...config.APP} /><Link to="/" className={styles.header}><img src={logo} alt="Logo" role="presentation" /><h1> <em>{config.APP.title}</em></h1> </Link><hr />{/* Child routes won't render without this */}{renderRoutes(route.routes)}</div>
};// src/client/index.tsx
const render = (Routes: RouteConfig[]) =>ReactDOM.hydrate(<Provider store={store}><ConnectedRouter {...props}>{renderRoutes(Routes)}</ConnectedRouter></Provider>,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<Props, State> {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 messagethis.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 pathreturn errorInfo ? (<div data-testid="error-view"><h2>Something went wrong.</h2><details style={{ whiteSpace: "pre-wrap" }}>{error && error.toString()}<br />{errorInfo.componentStack}</details></div>) : (children || null);}
}export default ErrorBoundary;
3.页面入口配置
const Home: FC<Props> = (): JSX.Element => {const dispatch = useDispatch();const { readyStatus, items } = useSelector(({ userList }: AppState) => userList,shallowEqual);// Fetch client-side data hereuseEffect(() => {dispatch(fetchUserListIfNeed());}, [dispatch]);const renderList = () => {if (!readyStatus || readyStatus === "invalid" || readyStatus === "request"return <p>Loading...</p>;if (readyStatus === "failure") return <p>Oops, Failed to load list!</p>;return <List items={items} />;};return (<div className={styles.Home}><Helmet title="Home" />{renderList()}</div>);
};
// 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 hereloadData: 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-cowindow.__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<void> => {const { store } = createStore({ url: req.url });// The method for loading data from server-side
const loadBranchData = (): Promise<any> => {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 firstawait loadBranchData();const statsFile = path.resolve(process.cwd(), "public/loadable-stats");const extractor = new ChunkExtractor({ statsFile });const staticContext: Record<string, any> = {};const App = extractor.collectChunks(<Provider store={store}>{/* Setup React-Router server-side rendering */}<StaticRouter location={req.path} context={staticContext}>{renderRoutes(routes)}</StaticRouter></Provider>);const initialState = store.getState();const htmlContent = renderToString(App);// head must be placed after "renderToString"// see: https://github.com/nfl/react-helmet#server-usageconst 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 responseif (staticContext.url) {res.status(301).setHeader("Location", staticContext.url);res.end();return;}// Pass the route and initial state into html template, the "statusCode" cres.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: {// 解析对应的loaderrules: [{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;
// loader style-loader postcss-loaderconst 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
}
// plugins csr ssr
const getPlugins = (isWeb: boolean) => {let plugins = [new webpack.ProgressPlugin(),// 适用于SSR服务下的manifest信息new WebpackManifestPlugin({}), // 改变ssr返回页面的titlenew LoadablePlugin({writeToDisk: true,filename: '../loadable-state.json', // 声明写入文件中的名称}),// 定义全局变量new webpack.DefinePlugin({__CLIENT__: isWeb,__SERVER__: !isWeb,__DEV__: isDev,})];// 根据process,env NODE_ENV analyzeif (!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 refreshnew 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 loadersallowlist: [/\.(?!(?: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,}),],
};export default merge(baseConfig(false), config);