遥远的桥 發表於 2020-6-19 18:54:00

react+ts搭建前端工程

<h2 id="前言">前言</h2>
<p>此文为ssr三部曲的第一部,前文在这<br>
这个版本的代码在这</p>
<h2 id="安装依赖">安装依赖</h2>
<h3 id="typescript">typescript</h3>
<p>安装typescript,并初始化一个tsconfig.json出来</p>
<pre><code class="language-bash">npm install -S -D typescript
node_modules/.bin/tsc --init    // 局部tsc需要这样使用
</code></pre>
<p>babel7开始,新增了@babel/preset-typescript,支持解析ts,所以不再需要ts-loader之类的webpack loader了</p>
<h3 id="webpack">webpack</h3>
<p>安装webpack、webpack-cli以及webpack-dev-server,webpack4开始webpack-cli与webpack分离成2个包了,分开维护。<br>
webpack-dev-server开发用,原理是用express起一个服务器,文件被build到内存中,通过socket跟client连接,达到热更新的目的。</p>
<pre><code class="language-bash">npm install -S -D webpack webpack-cli webpack-dev-server
</code></pre>
<h3 id="react">react</h3>
<p>安装react以及react-dom</p>
<pre><code class="language-bash">npm i -S react react-dom
</code></pre>
<p>再安装一下声明文件</p>
<pre><code class="language-bash">npm i -D @types/react @types/react-dom
</code></pre>
<h3 id="css">css</h3>
<p>css扩展是不可缺少的,我用sass最多,所以先安装sass相关依赖:</p>
<pre><code class="language-bash">npm install -S -D sass sass-loader css-loader style-loader mini-css-extract-plugin postcss-loader postcss-import postcss-preset-env postcss-pxtorem cssnano
</code></pre>
<ul>
<li>sass即为dart-sass,以前一般使用node-sass,安装中可能碰到各种问题,dart-sass能完美兼容并且避免这些问题</li>
<li>sass-loader用来把sass翻译成css</li>
<li>css-loader用来读取css文件(仅读取)</li>
<li>style-loader会把css用style标签包起来,放到header中,适用devlopment环境</li>
<li>mini-css-extract-plugin与style-loader作用类似,区别是使用link标签作为独立的文件引入,适用production环境</li>
<li>postcss-loader用来添加浏览器css兼容性代码</li>
</ul>
<blockquote>
<p>后面4个都是postcss的插件,作用如下:</p>
<ul>
<li>postcss-import用来处理css中的@import指令</li>
<li>postcss-preset-env跟babel类似,把新css语法转换为旧css语法</li>
<li>postcss-px2rem用来把px自动转换成rem</li>
<li>cssnano用来压缩代码</li>
</ul>
</blockquote>
<h3 id="babel">babel</h3>
<pre><code class="language-bash">npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread @babel/plugin-syntax-dynamic-import
</code></pre>
<ul>
<li>babel-loader和@babel/core就不用说了</li>
<li>preset-xxx(preset-env、preset-react、preset-typescript)都是babel预设的一些转码规则集,这3个分别用来转码es、react、ts
<blockquote>
<p>preset的顺序为从后至前</p>
</blockquote>
</li>
<li>@babel/plugin-xxx 都是babel的插件,会在preset之前执行,preset不支持的一些特性,就需要导入plugin了,这3个分别用来支持转码class、扩展运算符、动态导入
<blockquote>
<p>plugin的顺序为从前至后</p>
</blockquote>
</li>
</ul>
<p>此处必须提一下@babel/plugin-transform-runtime,runtime是提供沙箱垫片的方式来转换代码,而preset-env会覆盖全局环境,更具体的区别可以自行google:</p>
<ul>
<li>transform-runtime不会在每个文件中重复导入工具函数,会改从@babel/runtime中导入(runtime需要安装在dependencies中,而不是devDependencies)</li>
<li>preset-env可配置useBuiltIns方式:entry、usage</li>
<li>参考资料:preset-env官方文档、transform-runtime官方文档、简书博客</li>
</ul>
<h3 id="eslintprettier">eslint、prettier</h3>
<p>安装eslint及prettier:</p>
<pre><code class="language-bash">npm install -D eslint prettier eslint-plugin-prettier eslint-config-prettier eslint-plugin-react
</code></pre>
<blockquote>
<p>因为是react项目,所以除了eslint和prettier还装了eslint-plugin-react</p>
</blockquote>
<p>新增.eslintrc,填入以下内容:</p>
<pre><code class="language-js">{
    "extends": [
      "plugin:prettier/recommended"
    ]
}
</code></pre>
<p>eslint和prettier此处不做具体展开,请自行google,仅稍微说明一下eslint:<br>
eslint里有config和plugin两个概念,config配置规则(指定plugin对应的规则),plugin则定义具体的规则校验逻辑。<br>
所以eslint-plugin-prettier默认的配置是要定义plugin以及rule,里面嵌套的recommended则直接定义了config,所以直接可以把plugin:prettier/recommended扔到extends中来使用</p>
<p>上面配置之后,你会发现写ts,会提示各种语法错误,显然不识别ts的语法,所以我们还需要添加ts的支持:</p>
<pre><code class="language-bash">npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
</code></pre>
<p>最终的.eslintrc如下:</p>
<pre><code class="language-json">{
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
      "ecmaVersion": 2020, // Allows for the parsing of modern ECMAScript features
      "sourceType": "module", // Allows for the use of imports
      "ecmaFeatures": {
            "jsx": true
      }
    },
    "plugins": [
      "@typescript-eslint"
    ],
    "extends": [
      "plugin:react/recommended",
      "plugin:@typescript-eslint/recommended",
      "prettier/@typescript-eslint",
      "plugin:prettier/recommended"
    ],
    "env": {
      "browser": true,
      "node": true
    }
}
</code></pre>
<blockquote>
<p>更完善的格式化流程,还要配置husky和lint-staged,此处不做展开,可参考以下两篇文章:robertcooper、掘金<br>
有没有感觉很头大,这么多东西要配置。。</p>
</blockquote>
<h3 id="其他loaderplugin">其他loader、plugin</h3>
<pre><code class="language-bash">npm install -S -D url-loader html-webpack-plugin case-sensitive-paths-webpack-plugin clean-webpack-plugin
</code></pre>
<ul>
<li>url-loader用来处理其他文件的import/require,例如图片(把这些文件转发到生成目录,并解析成对应的url)
<blockquote>
<p>url-loader内置了file-loader,可以设置指定大小以下文件转为DataURI</p>
</blockquote>
</li>
<li>html-webpack-plugin用来生成一个html文件</li>
<li>case-sensitive-paths-webpack-plugin用来做路径大小写严格判断(mac上大小写不敏感,window和linux大小写敏感)</li>
<li>clean-webpack-plugin用来在每次打包之前清空目标目录(webpack的默认行为是增量,不清空)</li>
</ul>
<h3 id="开发调试">开发调试</h3>
<p>开发环境我们需要热更新,需要修改代码自动保存,这就需要用到我们之前安装的webpack-dev-server了,按照文档描述,配置devServer即可</p>
<p>最终结果,截止到目前为止,webpack的全部配置为以下两个文件:</p>
<ul>
<li>module-rules.js
<blockquote>
<p>loaders较多,所以单独写了一个文件</p>
</blockquote>
</li>
</ul>
<pre><code class="language-js">const isDev = process.env.NODE_ENV === 'development';
const miniCssExtract = require('mini-css-extract-plugin');
const postcssImport = require('postcss-import');
const postcssPresetEnv = require('postcss-preset-env');
const cssnano = require('cssnano');
const pix2rem = require('postcss-pxtorem');
const sass = require('sass');

const cssLoaders = [
    isDev ? 'style-loader' : miniCssExtract.loader,
    'css-loader',
    {
      loader: 'postcss-loader',
      options: {
            ident: 'postcss',
            plugins: (loader) =&gt; {
                const targetPlugins = [
                  postcssImport({ root: loader.resourcePath }),
                  pix2rem({ propList: ['*'], rootValue: 100 }),
                  postcssPresetEnv(),
                ]

                if (!isDev) {
                  targetPlugins.push(cssnano());
                }

                return targetPlugins;
            }
      },
    },
]

module.exports = () =&gt; {
    return [
      {
            test: /\.(js|jsx|ts|tsx)$/,
            exclude: /mode_modules/,
            use: [
                {
                  loader: 'babel-loader',
                  options: {
                        cacheDirectory: true,
                  }
                }
            ]
      },
      {
            test: /\.css$/,
            use: cssLoaders,
      },
      {
            test: /\.scss$/,
            use: [
                ...cssLoaders,
                {
                  loader: 'sass-loader',
                  options: {
                        implementation: sass,
                  }
                }
            ],
      },
      {
            test: /\.(png|jpg|jpeg|gif|svg|ttf)$/,
            exclude: /node_modules/,
            use: [
                {
                  loader: 'url-loader',
                  options: {
                        limit: 5*1024,// 5kb
                  },
                }
            ],
      }
    ]
}
</code></pre>
<ul>
<li>webpack.client.js</li>
</ul>
<pre><code class="language-js">const webpack = require('webpack');
const path = require('path');
const cwd = process.cwd();
const fs = require('fs');
const moduleRules = require('./module-rules');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CaseSensitivePathPlugin = require('case-sensitive-paths-webpack-plugin');

const isDev = process.env.NODE_ENV === 'development';
console.log('env is: '+process.env.NODE_ENV);

const plugins = [
    new webpack.ProgressPlugin(),
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      'process.env.DEPLOY_ENV': JSON.stringify(process.env.DEPLOY_ENV),
    }),
    new CleanWebpackPlugin(),
    new CaseSensitivePathPlugin(),
    new HtmlWebpackPlugin({
      template: path.resolve(cwd, 'webpack/index.html'),
      filename: 'index.html',
    }),
];
if(isDev){
    plugins.push(new webpack.HotModuleReplacementPlugin());
    plugins.push(new webpack.NamedModulesPlugin());
}


module.exports = {
    entry: path.resolve(cwd, 'src/client/app'),
    output: {
      path: path.resolve(cwd, 'dist/client'),
      filename: isDev ? 'js/..js': 'js/..js',
      chunkFilename: isDev ? 'chunks/..js' : 'chunks/..js',
      publicPath: '/',
    },
    // mode: process.env.NODE_ENV,// 由 --mode参数指定
    resolve: {
      extensions: ['.ts', '.tsx', '.scss', '.js', '.jsx', '.sass'],
      alias: {
            "@client": path.resolve(cwd, 'src/client'),
            "@server": path.resolve(cwd, 'src/server'),
      }
    },
    module: {
      rules: moduleRules(),
    },
    plugins,
    watch: isDev,
    devServer: isDev ? {
      contentBase: path.resolve(cwd, 'src/client'),
      compress: true,
      host: '0.0.0.0',
      port: 8080,
      hot: true,
      open: true,
      watchOptions: {
            ignored: /node_modules/,    // 监听过多文件会占用cpu、内存,so,可以忽略掉部分文件
            aggregateTimeout: 200,// 默认200,文件变更后延时多久rebuild
            poll: false,    // 默认false,如果不采用watch,那么可以采用poll(轮询)
      },
    } : undefined,
    devtool: isDev ? "inline-source-map": undefined,
};
</code></pre>
<ul>
<li>package.json中添加了以下两个命令:</li>
</ul>
<pre><code class="language-js">{
    "dev": "cross-env NODE_ENV=development DEPLOY_ENV=dev webpack-dev-server --mode=development --config webpack/webpack.client.js",
    "prd": "cross-env NODE_ENV=production DEPLOY_ENV=prd node_modules/.bin/webpack --mode=production --config webpack/webpack.client.js",
}
</code></pre>
<p>OK,开发和生产打包就都已经搞定了</p>
<h2 id="小结">小结</h2>
<p>这里是配置react,vue本质上也是一样的(webpack的核心、基础配置是不变的),换上vue相关的webpack loader,vue相关的eslint插件,等等。</p>
<p>从0配置spa前端项目,到这里这就已经完成了,整体来看,配置的东西的确很多,但也都是作为工程化所需要的东西。<br>
说到底,也是因为前端没有一个统一的、完善的环境工具,对于后端来说,比方说.net,一个visual studio搞定一切,美滋滋,哪有这么多事。。</p>


</div>
<div id="MySignature" role="contentinfo">
    本文在我的博客园和我的个人博客上同步发布,作者保留版权,转载请注明来源。<br><br>
来源:https://www.cnblogs.com/thyong/p/13164624.html
頁: [1]
查看完整版本: react+ts搭建前端工程