Loading... <div class="tip inlineBlock share"> 上篇文章写的有些简略,而且有些地方在实际开发中还是有一定问题存在的,所以就有了这个进阶篇,这篇文章也是因为最近在搞前端开发,所以顺便总结一下 </div> ## 大纲 ## 首先我先说明一下这次使用的技术栈: - less css预处理器,知名的还有sass、scss、stylus等 - react 主要的开发依赖库 - typescript 提供静态类型等诸多好处 - webpack 打包工具 - webpack-dev-server 开发用服务器 - react-router 路由管理库 - axios 网络请求库 - babel es新特性等代码的polyfill 以这些作为主要的开发工具和生产依赖,除此之外还有一些关于webpack打包的插件,babel的插件之类的都会在后续进行一一说明 ## 步骤 ## 首先,我们从新建文件夹开始 ```bash mkdir my-app \ && cd my-app \ && npm init -y ``` 然后我们就开始装依赖了 ```bash npm install react react-dom react-router-dom axios --save-exact ``` 以上是几个生产环境所需要的依赖,所以放在一起进行安装,这里的`--save-exact`是为了把下载下来的依赖库版本固定,从而避免不同人开发时因为版本更新导致的诸多问题 现在我们开始安装开发方面的依赖库,这里要使用一个参数`--save-dev`,说明这些依赖只在开发时使用 ```bash npm install @babel/core @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react @babel/runtime @babel/preset-typescript --save-dev \ && npm install webpack webpack-cli webpack-dev-middleware webpack-dev-server webpack-merge --save-dev \ && npm install typescript @types/react @types/react-dom @types/webpack @types/node ts-node --save-dev \ && npm install less less-loader css-loader babel-loader style-loader --save-dev \ && npm install clean-webpack-plugin css-minimizer-webpack-plugin fork-ts-checker-webpack-plugin html-webpack-plugin mini-css-extract-plugin --save-dev ``` 这里可以看到,有非常多的开发方面的依赖,在这里我一行一行的进行解释 首先第一行,我们可以看到都是关于babel方面的一些库,下面是它们的作用 - @babel/core babel的核心库 - @babel/plugin-transform-runtime 和 @babel/runtime 提供对async/await功能的支持 - @babel/preset-env 提供对ES规范新功能的支持 - @babel/preset-react 提供对react相关代码规范的支持 - @babel/preset-typescript 提供对typescript代码的支持 babel主要的作用就是为了提供对一些浏览器不支持的语法进行降级转换的作用,使得其可以在浏览器运行 然后是第二行,关于webpack的一些功能模块 - webpack 和 webpack-cli 一个是核心库,一个是命令行支持 - webpack-dev-middleware 和 webpack-dev-server 对开发环境提供热替换开发服务器功能 - webpack-merge 用于对配置文件的merge,方便进行配置文件的区分管理(ts原生也可以合并,但是这个比较智能) 第三行,关于typescript方面的一些模块声明文件还有typescript自身,这里就不做详细解释了 第四行,提供了less支持和一些webpack读取相关文件的相应loader 第五行,关于webpack的一些插件 - clean-webpack-plugin 在打包生成文件时会对之前的文件进行相应的清理操作,主要是为了修复webpack-dev-middleware在启用writeToDisk功能时,webpack原生配置启用自动清理却无法清理的bug - css-minimizer-webpack-plugin 对打包生成的css进行最小化压缩处理 - fork-ts-checker-webpack-plugin 提供在打包编译前先对源代码进行ts类型检查的功能 - html-webpack-plugin 提供对html模板的处理功能,比如改改标题,挂载打包生成的js和css文件之类的 - mini-css-extract-plugin 将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载。 <div class="tip inlineBlock info"> 这里的SourceMaps实际上与压缩时采用的一种技术有关,因为压缩后生成的文件的可读性非常差,而SourceMaps其实就相当于一本密码本可以把压缩前的源文件给复原出来,从而方便开发人员调试,具体压缩时采用了VLQ编码,感兴趣的同学可以去看看阮一峰的这篇[文章][1] </div> 到这里,我们就把所安装的所有依赖给说明完毕了,接下来就是这些依赖的配置篇了 ## 配置 ## ### babel的相关配置 我们首先在项目根目录下创建一个.babelrc文件 然后填入以下内容 ```json { "presets": [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript" ], "plugins": [ [ "@babel/plugin-transform-runtime", { "regenerator": true } ] ] } ``` 很容易理解,上面三项是关于上述的一些预设的转换规则的填入,而这里的plugin中的regenerator配置项,是有关js的generator函数方面的,也即形如下面的函数 ```javascript function* name([param[, param[, ... param]]]) { statements } ``` 这方面的内容可以参考[MDN][2] 这里的配置主要是为了防止形如上述的函数污染全局变量域 ### webpack的配置 因为我们引入了webpack的types和ts-node,所以我们可以使用typescript进行webpack相关配置文件的编写,同时因为我们引入了webpack-merge,我们可以针对生产环境和开发环境采用不同的配置文件。 所以我们可以创建webpack.common.ts webpack.dev.ts和webpack.prod.ts三个文件 以下是common相关配置 ```typescript import HtmlWebpackPlugin from "html-webpack-plugin"; import { CleanWebpackPlugin } from "clean-webpack-plugin"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import path from "path"; import { Configuration } from "webpack"; const commonConfig: Configuration = { entry: { app: ["./src/index.tsx"], }, output: { path: path.resolve(__dirname, "dist"), }, resolve: { alias: { "@": path.resolve(__dirname, "src"), "@component": path.resolve(__dirname, "src/components"), "@page": path.resolve(__dirname, "src/pages"), "@lib": path.resolve(__dirname, "src/libs"), }, extensions: [".ts", ".tsx", ".js", "jsx", ".json"], }, module: { rules: [ { test: /\.(ts|js)x?$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: [ "@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript", ], }, }, }, { test: /\.(png|jpg|gif)$/, type: "asset/resource", generator: { filename: "images/[hash][ext][query]", }, }, { test: /\.svg$/, type: "asset/inline", }, ], }, plugins: [ new HtmlWebpackPlugin({ title: "my_app", filename: "index.html", template: "./public/index.html", }), new CleanWebpackPlugin(), new ForkTsCheckerWebpackPlugin({ async: false, //set this to false so that Webpack waits for the type checking process to finish before it emits any code. }), ], }; export default commonConfig; ``` 可以看到,我们这里引入了三个plugin,作用在上面已经解释过了 然后我们看到entry就是相关的入口文件,在react里面,我们一般就会有以下的入口文件定义 ```tsx import * as React from "react"; import * as ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import App from "@/App"; ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById("app") ); ``` 然后关于相关的html模板则会有如下的定义 ```html <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" /> <style> #app { width: 100%; height: 100%; } </style> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <div id="app"></div> </body> </html> ``` 这里有一个`<%= htmlWebpackPlugin.options.title %>`,这个是为了能够让html-webpack-plugin中配置的标题能够注入进模板中 而这里关于所谓的@别名其实就与webpack中配置的resolve中的alias有关了,具体配置方法可以参照以上配置文件,但是需要注意的是,仅仅只在webpack配置文件配置这个别名是不够的,还必须在tsconfig.json中有相关的配置,这里给出tsconfig的配置文件 ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"], "@page/*": ["src/pages/*"], "@component/*": ["src/components/*"], "@lib/*": ["src/libs/*"] }, "lib": [ "dom", "DOM.Iterable", "esnext" ] /* Specify library files to be included in the compilation. */, "allowJs": true, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, "sourceMap": true /* Generates corresponding '.map' file. */, "outDir": "./dist/" /* Redirect output structure to the directory. */, "removeComments": false /* Do not emit comments to output. */, "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "preserveConstEnums": true, "resolveJsonModule": true, "moduleResolution": "node" /* Specify module resolution strategy: 'node', 'classic' or 'none'. */, "forceConsistentCasingInFileNames": true /* Ensures that the casing of referenced file names is consistent during the type checking process. */, "skipLibCheck": false, "isolatedModules": true, "strict": true, "noEmit": true, "typeRoots": [ "node_modules/@types" ] /* List of folders to include type definitions from. */ }, "include": ["src/**/*"] } ``` 可以看到,我们需要在paths中配置相关的别名,这里主要解释几个配置项,一个是 include ,配置我们需要读取的源文件在哪里,还有就是 lib ,告诉编译器我们需要编译出的js代码的对象是提供给浏览器使用的,还有就是编译成最新的es规范代码,其余的一些配置项详情可以在以下页面进行[查看][3] 然后回到我们的webpack配置,可以看到module里面有关于不同文件的不同处理方式,这些东西大多参照自webpack官方文档,没有什么非常需要解释的,总的来说,就是对匹配到的不同类型的文件进行不同的打包处理,而关于 resolve 下的 extensions ,其实就是定义了同名文件的解析顺序,如果有多个文件的名字相同而类型不同,就会按照定义的顺序进行解析,如果解析成功就会跳过之后的文件后缀名。 然后再来讲讲开发环境下webpack的特殊配置 ```typescript import { merge } from "webpack-merge"; import { Configuration as WebpackConfiguration } from "webpack"; import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server"; import commonConfig from "./webpack.common"; interface Configuration extends WebpackConfiguration { devServer?: WebpackDevServerConfiguration; } const devConfig: Configuration = merge(commonConfig, { mode: "development", output: { filename: "js/[name].[hash].bundle.js", }, module: { rules: [ { test: /\.css$/i, use: ["style-loader", "css-loader"], }, { test: /\.less$/i, use: ["style-loader", "css-loader", "less-loader"], }, ], }, devtool: "inline-source-map", optimization: { runtimeChunk: "single", }, devServer: { historyApiFallback: true, devMiddleware: { writeToDisk: true, }, hot: true, }, }); export default devConfig; ``` 这里由于webpack自身和webpack-dev-server的配置文件类型中有所不同,所以为了包含webpack-dev-server的配置文件,在这里自己定义了一个接口来进行整合处理。 然后这里关于dev-server的配置,historyApiFallback是为了适配react-router的特殊路由系统,hot则是启用HMR(热模块重载)功能,然后就是重新定义了输出的js文件名和关于样式文件的处理方式与生产环境有所不同 ```typescript import { merge } from "webpack-merge"; import commonConfig from "./webpack.common"; import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; import MiniCssExtractPlugin from "mini-css-extract-plugin"; import { Configuration } from "webpack"; const prodConfig: Configuration = merge(commonConfig, { devtool: "source-map", mode: "production", output: { filename: "js/[name].[contenthash].bundle.js", }, module: { rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"], }, { test: /\.less$/i, use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: "css/[name].css", }), ], optimization: { minimizer: [new CssMinimizerPlugin()], splitChunks: { chunks: "all", }, }, }); export default prodConfig; ``` 总体来讲,这两种配置的差异主要就是输出的js文件命名方式不同,还有样式文件的输出方式不同,除此之外就只有sourcemap的生成方式不同 配置完了这些,我们就可以来配置几个快速指令了,打开package.json,在scripts写入以下配置 ```json { "start": "webpack-dev-server --open --config webpack.dev.ts", "build": "webpack --config webpack.prod.ts" } ``` 然后我们就可以使用`npm start`来启动这个项目了,当然在此之前,我们还得写一个App.tsx,我想这个应该就不用赘述了。 ## 效率提高 ## 为了统一的代码风格,我们可以引入prettier进行代码格式化管理 ```bash npm install --save-dev prettier ``` 然后在项目根目录创建.prettierrc.json和.prettierignore .prettierrc.json写入以下内容 ```json { "trailingComma": "es5", "tabWidth": 4, "semi": true, "singleQuote": false } ``` .prettierignore填入 ```ignore node_modules # Ignore artifacts: dist ``` 关于prettier的具体配置可以查看[官网][4] ## 最后 ## 这段时间也是正好在写一个前端的项目,自己也翻了无数的资料文档,最终才总结出来了这么一些配置,前端的工具真的是太多太多了,所以也是让人看的眼花缭乱,光是一个webpack的配置就已经非常的复杂了,我看了好几天的webpack文档,通过一点点的去尝试修改,才最终获得了一个满意的开发环境。可以说,从零开始配置前端开发环境是真的非常困难的一件事情,在如今这样一个前端工具漫天飞的时代,也许脚手架才是更好的选择,但是偶尔停下脚步,去探究一下黑盒之下的东西,也是为了更好的理解脚手架的工作,以及懂得脚手架带来的便利是一个多么伟大的进步。 [1]: https://www.ruanyifeng.com/blog/2013/01/javascript_source_map.html [2]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* [3]: https://www.typescriptlang.org/tsconfig [4]: https://prettier.io/docs/en/configuration.html Last modification:February 14, 2022 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 如果觉得我的文章对你有用,请随意赞赏