13.JS学习篇-ES6 React 项目模板

1.项目能力支持

1.项目初始化脚手架

1.前端编码规范工程化(lint工具、Node CLI等)

2.用工具提升项目的编码规范,如:eslintstylelintcommitlintmarkdownlinthusky等

3.工具对于JavaScriptTypescriptReactVue等不同类型的前端项目下的标准的语法限制;

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.项目初始化配置

项目目录:

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 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 ? (
            <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 here
    useEffect(() => {
        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 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<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 first
    await 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-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,
    }),
  ],
};

export default merge(baseConfig(false), config);

  • 35
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值