The introduction

Recently, I started my own blog, because it is my own project and not in a rush of schedule and time, and there is no need to do many fancy things, so I don’t plan to use the excellent ready-made build tools on the Internet to start the project, but start from Webpack + React to build a simple pseudo-engineering front-end project, by the way, I also know more about Webpack. And then the actual development in the grope and improve the project bit by bit

CHANGE LOG

2020.5.17

  • The introduction of eslint
  • Optimize the WebPack configuration
  • Fixed a bug in @babel/ transform-Runtime that had been pointed out by the boss
  • Fixed ts path alias error

2020.5.30

  • Implement the Markdonw editor
  • Webpack images smaller than 20KB are packaged into Base64 to reduce the number of resources

webpack

Talking about Webpack, we have to talk about front-end engineering. As JavaScript continues to get bigger and stronger, the front end is not just about page presentation and page effects, but more and more user interaction and user experience optimization, and some business scenarios can be freed from the back end to the front end. Front-end engineers are no longer faced with simple websites, but with more complex Web applications, which means that front-end project construction must be built in a more systematic engineering direction.

Think of the beginning of the pre-school, a simple page and a file to complete + HTML/CSS/JS all written together, there is no doubt that this development efficiency is very low and will do a lot of redundant and repetitive work, so we need to treat the front-end project as a system engineering for analysis, organization and construction, In the development level to do more componentization, modularization, standardization and so on, make the project structure clear, reduce redundant workload and improve work efficiency.

In fact, front-end engineering also involves build/deploy/test/automation/performance/stability/availability/maintainability, including some of the deployment/release /CI/CD/ grayscale concepts that can not be explained in a word

From this background, a series of good front-end build package modular tools such as Grunt/Gulp/Webpack/Rollup have been born. Each tool has its own scenarios and features that are not as good as the others. This article focuses on the idea of building a front-end project with WebPack and some common optimization points

Pick up the concept of Webpack

In essence, Webpack is a static Module bundler for modern JavaScript applications. When WebPack works with an application, it recursively builds a Dependency graph containing every module the application needs, and then packages all of those modules into one or more bundles.

Steal the webpack diagram again

The concepts and images above are enough to convey the idea and function of WebPack, but to sum it up in one sentence: in front-end engineering, we just develop and build everything to WebPack.

There are many excellent articles about how to build a project from 0. This article also learns from the big guy’s article to learn how to build a simple React project using Webpack ===>, so it also directly skim my superficial understanding of webpack and enter the construction process directly

Build the project

As the saying goes, nothing can be accomplished without rules and regulations. Before starting a project, you need to first think about the original intention, expectations and possible force majeure factors (such as suddenly stopping writing on that day…). . So before initializing the project, you need to do a simple plan for the project to facilitate subsequent development iterations and maintenance

The project structure

├─ dist Compiled project file ├─ Node_modules ├─ Public Static Resource File ├─ Script Build file │ ├─ Analyser.js Size │ ├─ build.js Package │ ├─ │ ├─ WebPack.base.config.js ├─ SRC source │ ├─ Pages Component │ ├─... Other │ ├─ Index.tsx import File ├─.babelrc Config ├─.eslintrc.js esLint Config ├─.Gitignore Ignore Submit to Git directory ├─.prettierrc ├─ Tsconfig. json Typescript ├─ Readme. md Description fileCopy the code

Because the focus of this article is on theory, so the source code is relatively little. So here is a brief description of each file under script:

  • webpack.base.config.js

    This is a basic Webpack file with basic configuration including entry, output, parsing module, code compression and unpacking, HTML template, etc. Why is it needed to be taken out, mainly for two reasons

    1. Remove common code to make code more concise
    2. Ensure that the development and production environments are as consistent as possible, and avoid meaningless problems caused by environmental differences
  • start.js

    The gateway to the project development environment startup, which aggregates basic configuration and development-time configuration, such as devServer/HMR and other development environment configuration items

    plugins: [
      new webpack.HotModuleReplacementPlugin(),
      new webpack.NoEmitOnErrorsPlugin()
    ]
    Copy the code
  • build

    The project packages the build entry, including some build performance monitoring code, in addition to basic configuration. For example, using HappyPack to speed up packing:

    new HappyPack({
      threads,
      id: 'jsx'.loaders: ['babel-loader']}),Copy the code
  • dll.js

    Remove third-party modules to reduce build volume

  • analyzer.js

    After the completion of the package construction, it is necessary to make a detailed analysis of the package split size, which is conducive to further optimization of the unpacking and code structure adjustment

I. Technology stack

There are three main frameworks react/ Vue/Angular. Each framework has its own applicable and non-applicable scenarios, as well as the ecosystem, learning cost, stability and ability to work with infrastructure.

Since it is a personal project, I will directly consider typescript+ React family development, which is relatively easy to configure

// Babel compiles. Babelrc configuration
{
  test: /.jsx? $/.exclude: /(node_modules|bower_components)/.loader: 'babel-loader'
},
/ / use the typescript
{
  test: /\.(ts|tsx)$/.exclude: /node_modules/.use: {
  	loader: 'ts-loader',}}// .babelrc
{
  "presets": [
    "@babel/react"."@babel/preset-env"]."plugins": [
    "@babel/plugin-transform-runtime"]}Copy the code

Ii. Compatibility

There are two main strategies for front-end compatibility

  • Progressive enhancement

    Progressive enhancements guarantee the experience of older browsers and provide a slightly better experience for newer browsers that support new features

  • Graceful degradation

    Elegant downgrading is the opposite, providing the best experience for modern browsers, while older browsers take a back seat and keep up with the basics

This is an elegant downgrading strategy (since compatibility with older browsers is not really considered).

3. Coding specifications

The coding specification of the project is also a platitude topic. Good coding habits are conducive to long-term maintenance of the project, improve development efficiency and code quality, reduce the burden of legacy system maintenance, and reduce the technical debt caused by garbage code.

Commonly used and recommended on the web are ESLint to restrict code specifications, Prettier to Prettier, and husky to restrict submissions

But because I was confident enough in my coding style, I didn’t use any of them

Fourth, front-end monitoring and optimization

A mature website, there must be supporting a mature monitoring system to support. A good monitoring system can not only help me to do the pv/ UV/user habits/user behavior analysis and statistics of the website, but also have the ability to timely find system/processing system abnormalities, troubleshooting errors, and avoid serious production failures.

Although it is a personal salted fish site, but the dream is still some, considering that I may fire later, so nip in the bud to some online monitoring statistics system. After searching around, it either costs money (pocket empty, no budget), or fails to find the desired effect, so I wrote several buried interfaces (refer to the interface implementation part in the previous article), which is better than nothing. It is only used for monitoring anomalies and statistical analysis, which is convenient for the continuous optimization of the subsequent system

4. Some non-important specifications in this project

There are a number of other specifications that are important for a team project, but since this project is an individual project, I’ll briefly mention them here:

  1. Code branch management
  2. The process specification
  3. Collaborative specification
  4. Reasonable and clear division of labor
  5. Publish the build process
  6. Code quality specification
  7. Code style specification
  8. Code refinement/review
  9. Test specification
  10. Ongoing maintenance plan
  11. Document specification
  12. .

Five, some optimization points

Dynamic introduction

Common modules, such as routing or redux, can be introduced dynamically through webpack’s require.context module

  1. Dynamic Route Import

    let RouteMap = []
    / / white list
    const WHITE_LIST = ['login']
    // NOTE:[XXX /:id] is not considered by default. How to solve this situation
    // dynamically read all directories under Pages and treat each directory as a route
    const files = require.context('./pages'.true, /\/index\.jsx$/)
    files.keys().forEach(key= > {
      // const pattern = /(? < = \ /) + (? = \ /)
      const pattern = /\.\/(.+)\/(\w+)\.jsx? $/
      const route = key.match(pattern)[1]
      // The whitelist route is skipped
      if(! WHITE_LIST.includes(route)) { RouteMap.push({path: route,
          component: files(key).default
        })
      }
    })
    
    const LayoutRoute = (props) = > (
      <Layout {. props} >
        <Switch>
          { RouteMap.map(({path, component}) => <Route key={path} exact path={` /app/ ${path} `}component={component} />)}</Switch>
      </Layout>
    )
    Copy the code

    In this way, there is no need to import a route file every time and map the route file to the corresponding route. As long as there are many rules and good rules, even the step of configuring the route file can be realized in this way.

  2. Dynamically register the Redux module

    Automatically register the Reducer module
    let rootReducer = {}
    const reducersFiles = require.context('./reducers'.false, /\.js$/)
    reducersFiles.keys().map(filename= > {
      // const pattern = /(? < = \. \ /) + (? =\.js)/ new syntax, some browsers do not support it
      const pattern = /\/(\w+)\.jsx? $/
      try {
        const matchRes = filename.match(pattern)
        const key = matchRes[1]
        rootReducer[key] = reducersFiles(filename).default
      } catch (e) {
        console.log('Unable to register', e)
      }
    })
    
    // Automatically register the saga module
    let rootSagaList = []
    const sagaFiles = require.context('./sagas'.false, /\.js/)
    sagaFiles.keys().map(filename= > {
      rootSagaList.push(fork(sagaFiles(filename).default))
    })
    Copy the code

By requiring. Context, you can eliminate a lot of unnecessary and repetitive code, at least without importing module files. You can learn the DVA idea and wrap it up again, but that’s another story.

Babel optimization

Because of compatibility issues between browsers and the time difference between browsers implementing the W3C specification, in case the API we are using is not yet implemented on the browser, a common front-end project will introduce Babel to do a compilation. The introduction of this compilation tool also needs to be able to make some simple optimizations to the project structure

  1. Eliminate node_modules/bower_components compilation, exclude: / node_modules | bower_components) /

    // webpack.base.config.js
    {
      test: /.jsx? $/.exclude: /(node_modules|bower_components)/.loader: 'babel-loader'
    }
    Copy the code
  2. Use @babel/ plugin-transform-Runtime to extract common modules and reduce the size of the code

    // .babelrc
    "plugins": [
      "@babel/plugin-transform-runtime"
    ]
    Copy the code
  3. Use happypack to start multiple threads

    Because webpack is single-threaded, sometimes we can’t take full advantage of modern CPU multi-core, so we need to find a way to take advantage of this advantage to improve the efficiency of packaging and compilation. HappyPack can do this by splitting tasks into multiple sub-processes for concurrent execution

    const HappyPack = require('happypack')
    // Manually create a process pool
    const happyThreadPool =  HappyPack.ThreadPool({ size: os.cpus().length })
    
    module.exports = {
      module: {
        rules: [{...test: /\.ts$/.// The query argument after the question mark specifies the name of the HappyPack instance that processes such files
            loader: 'happypack/loader? id=happyBabel'. },],},plugins: [
        ...
        new HappyPack({
          // The "name" of the HappyPack is happyBabel
          id: 'happyBabel'.// Specify a process pool
          threadPool: happyThreadPool,
          loaders: ['babel-loader? cacheDirectory']})],}Copy the code

Package performance monitoring

If the packaging volume is too large, the page loading time will be very long, which is a very bad experience, so we need a prompt mechanism, when we package the file is too large, we can timely know the file size, Webpack also provides a very convenient function performance

performance: {
  hints: 'warning'.// Prompt type
    // If a resource exceeds 200KB after creation, a warning will be displayed
    maxAssetSize: 1024 * 200.maxEntrypointSize: 1024 * 200,}Copy the code

In this way, we can detect large packages in packaging and further optimize packaging and development

Disengage third-party dependencies

Webpack provides a nice feature, DllPlugin, that does a great job of pulling out third-party dependencies and producing a mapping file called manifest.json

const OUTPUT_PATH = BUILD_OUTPUT_DIR ? `${BUILD_OUTPUT_DIR}/lib/` : path.resolve('dist/lib')
// ...
module.exports = {
  // ...
	plugins: [
		// Remove package plugins
    new webpack.DllPlugin({
			context: __dirname,
			path: path.resolve(OUTPUT_PATH, 'manifest.json'),
			name: '[name]-[hash]'}})]Copy the code

After the package is extracted, the package needs to be used through the mapping file, and then the DllReferencePlugin is used to map the dependencies to the project, and then the dependencies can be used normally

new webpack.DllReferencePlugin({
  context: __dirname,
  sourceType: "commonjs2".name: 'lib_dll'.manifest: dllMap / / the manifest. Json address
})
Copy the code

splitChunks

Webpack 4.x provides splitChunks for easier unpacking, which is an improved version of the DllPlugin

Tips: I don’t know if it’s a configuration problem, some packages will be packed twice when used at the same time, which will increase the size

optimization: {	
		// Pack and unpack
    splitChunks: {
      // This indicates which blocks will be selected for optimization. When supplied with a string, valid values are all, async, and initial. Providing all can be particularly powerful because it means that blocks can be shared even between asynchronous and non-asynchronous blocks.
      chunks: 'all'.// Minimum block size (in bytes) to produce
      minSize: 10240.maxSize: 0.// The minimum number of blocks that must be shared before splitting
      minChunks: 1.// Maximum number of parallel requests when loading on demand
      maxAsyncRequests: 5.// Maximum number of parallel requests at the entry point
      maxInitialRequests: 3.// Specify the dividers used to generate the names, vendors~main.js
      automaticNameDelimiter: '~'.// The name of the split block
      name: true.cacheGroups: {
        / / out CSS
        // styles: {
        // name: 'static/css/styles',
        // test: /\.(css|scss|sass)$/,
        // chunks: 'all',
        // enforce: true,
        // },
        // Extract the public module
        commons: {
          name: 'static/js/components'.test: path.join(__dirname, '.. '.'src/components'),
          minChunks: 3.priority: 5.reuseExistingChunk: true,},// Extract react separately
        react: {
          test: /[\\/]node_modules[\\/](react)[\\/]/.name: 'static/js/react'.priority: 20,},// Extract the react-dom separately
        reactDom: {
          test: /[\\/]node_modules[\\/](react-dom)[\\/]/.name: 'static/js/react-dom'.priority: 20,},// Pull out the third party's package
        vendors: {
          name: 'static/js/vendors'.test: /[\\/]node_modules[\\/]/.priority: 15,},default: {
          minChunks: 2.priority: - 20.reuseExistingChunk: true,},},},}Copy the code

The react-dom/react/ dependencies can be used to generate three separate files. If there are other dependencies that are too large, you can also add rules to remove them

Six, the follow-up plan

  1. Continuously optimize the WebPack configuration
  2. Introducing esLint specification code
  3. Introduce JEST for unit testing
  4. Build cache policies to optimize the user experience
  5. Retrofit project for SSR/server rendering
  6. Pull out common components and build component libraries
  7. Optimize the front-end monitoring module
  8. .

conclusion

Paper always feel shallow, must know this to practice, before see others casually build a project feel no what, when it is their turn to do feel bald hair cool, only in this article, pay homage to those away from my hair.

👏 Github storage address

👏 interested partners if found in the article is wrong/insufficient/need to improve/can be optimized, I see it will be updated in time, let us become better together!

👏 if the article is helpful to you, please click 👍 and go again!