I've been working on the mini-program Shanke Station for two years, and it's been built using uniapp. During this period, I've refactored it several times. During my internship at a certain company, I gained a lot, but now I need to do a major refactoring again. Although the functionalities of the mini-program are relatively simple, I need to apply what I've learned during the internship. So, let's continue working on the mini-program. The most significant gain from the internship was not how to migrate to TS, but rather the concepts of component design and directory structure design. However, these will be things to address when rewriting the components later on.
Back to the main topic, the mini-program is written using uniapp, and since I'm quite familiar with Vue syntax, the first step in the migration is to move the mini-program from HBuilderX to the cli version. Although HBuilderX does have certain advantages, its extensibility is relatively poor. I need to tinker with it myself, so after completing the migration to the cli version, the next step is to gradually transition from js to ts. Although Vue2 has relatively poor support for ts, at least the logic that's been abstracted can be written in ts, thus avoiding many errors during the compilation. Additionally, creating one's own functionality using cli may be possible, provided that there's no manipulation of the DOM. Generally, common js methods are still used. For example, trying to integrate Jest unit testing, among other capabilities.
The first step is to migrate to the cli version. Although the official website explains how to create a uniappcli version, there are still many pitfalls.
Initially, there's no issue with installing dependencies using npm and yarn, but using pnpm may lead to situations where compilation fails. After various tests, no results were found, almost as if there's an internal exception, which was caught by the webpack plugin written by uniapp, and no exception information was thrown outward. It's quite frustrating. I've been using pnpm to manage packages all along, but now I have to use yarn to manage the entire project. Additionally, my attempt to use a symbolic link, mklink -J, to create a central package storage failed. The dist folder generated by the plugin is located in a very strange place, causing the build process to fail as it couldn't find the folder path, ultimately leading to a compilation failure. So, if you want to use uniapp's cli, you can only follow the standard procedures and not engage in fancy operations.
First, install vue-cli globally:
$ npminstall -g @vue/cli
Create the project project:
$ npminstall -g @vue/cli
Next, choose the version and select the default template for TypeScript, so that you don't need to configure things like tsconfig.json. Afterwards, the existing code needs to be moved to the src directory of the new project. Of course, configuration files such as .editorconfig still need to be moved out and placed in the root directory. If some plugins, such as sass, are not configured, the mini-program may be able to run now. If you've installed other plugins, pay close attention to the dependency issues, because some of these plugins written in uniapp may have quite outdated dependencies, requiring installation of older version plugins for compatibility.
As mentioned earlier, directly yarn install -D xxx may lead to problems. For example, I encountered issues with sass and webpack version incompatibility. Additionally, code standardization plugins such as eslint and prettier need to be installed, along with eslint's ts parser and plugins. All of this has already been configured and runs smoothly in VS Code. I've also configured lint-staged and more. Here's the information in the package.json file. With this file available, you can directly start a normal, compilable uniapp-typescript template. If you need other plugins, you'll need to try them yourself.
// src/modules/datetime.tsexportfunctionsafeDate(): Date;exportfunctionsafeDate(date: Date): Date;exportfunctionsafeDate(timestamp:number): Date;exportfunctionsafeDate(dateTimeStr:string): Date;exportfunctionsafeDate( year:number, month:number, date?:number, hours?:number, minutes?:number, seconds?:number, ms?:number): Date;exportfunctionsafeDate( p1?: Date |number|string, p2?:number, p3?:number, p4?:number, p5?:number, p6?:number, p7?:number): Date |never{if(p1 ===void0){// Construct with no parametersreturnnewDate();}elseif(p1 instanceofDate||(typeof p1 ==="number"&& p2 ===void0)){// The first parameter is a `Date` or `Number` and there is no second parameterreturnnewDate(p1);}elseif(typeof p1 ==="number"&&typeof p2 ==="number"){// Both the first and second parameters are `Number`returnnewDate(p1, p2, p3, p4, p5, p6, p7);}elseif(typeof p1 ==="string"){// The first parameter is a `String`returnnewDate(p1.replace(/-/g,"/"));}thrownewError("No suitable parameters");}
typeDateParams=|[]|[string]|[number,number?,number?,number?,number?,number?,number?]|[Date];const safeDate =<TextendsDateParams>(...args:T): Date =>{const copyParams = args.slice(0);if(typeof copyParams[0]==="string") copyParams[0]= copyParams[0].replace(/-/g,"/");returnnewDate(...(args as ConstructorParameters<typeof Date>));};
It's quite tricky to write TypeScript in Vue files. In fact, there are two main ways. One is using Vue.extend, and the other is using decorators. The primary reference I used is this. Personally, I lean towards the decorator approach. However, when using decorators to write components in WeChat mini-programs, a prop type mismatch warning often occurs. It doesn't affect usage. Regardless of the approach, there are still fragmentation issues. This can be considered a design flaw in Vue 2, especially since TypeScript wasn't very popular at that time.
Writing and publishing a component to NPM in uniapp is quite tricky. I'd like to extract some functionalities into a standalone NPM package for multi-project usage, but there are numerous hurdles to overcome. Here, I mainly record the pitfalls encountered, which can be quite frustrating. Since it is mainly used on the mini-program side, it's different from the web side. It needs to be compiled into files that the mini-program can recognize. However, dcloud currently does not provide this capability, so it can only write the most original vue components. Also, since uniapp performs many plugin parsing behaviors, some things are even directly fixed in the code and cannot be changed externally. In addition, some error locations do not throw exceptions but swallow them directly, resulting in the output file being empty without any console prompts. In short, there were quite a few pitfalls to overcome. Here, there are three ways to complete the NPM component publishing. I use https://github.com/WindrunnerMax/Campus as an example.
First is the simplest way, similar to https://github.com/WindrunnerMax/Campus/tree/master/src/components. All the components are completed in the components directory. We can directly create a package.json file here, and then publish the resource files here. In this way, it's very simple. When using it, just reference it directly. Also, you can set an alias to reference it, I tried it in VSCode, there will be code prompts when pressing @, so adding an @ as an alias may be helpful.
The second method is quite tricky, and to be honest, I've given up on this idea now. However, I'll still document the process because after all, I've managed to achieve a functional implementation after toiling for a whole day. But it's not very versatile, mainly because the regular expression matching in the loader doesn't cover all scenarios. So, ultimately, I didn't opt for this method. What initially seemed like a straightforward issue ended up requiring the creation of a loader, which was quite a headache. Initially, I aimed to achieve a import format similar to import { CCard } from "shst-campus". It looks familiar, and the idea was to mimic the import method of antd or similarly element-ui. So, in reality, I did delve into their import methods, and in the end, I created a Babel plugin. Through this plugin, the imports are compiled into other import statements. By default, the example I mentioned earlier would look something like import CCard from "shst-campus/lib/c-card". Of course, this can be configured using babel-plugin-import and babel-plugin-component to achieve a kind of demand-driven loading. First, I tried babel-plugin-import and configured the related paths.
The idea was idealistic, but when I attempted to compile, I found that this configuration had no effect. Although I was puzzled, I considered that this was originally a built-in plugin for uniapp, so the configuration might have been overridden or disregarded. Hence, I tried using babel-plugin-component.
This time, it actually had an effect and succeeded in achieving demand-driven loading. Excitedly, I proceeded with the compilation which was successful. However, upon opening the WeChat developer tool, I encountered an error. It turned out that there was an error in the json file, as the imported component could not be found. In the json file, the imported file was simply placed as is, i.e., shst-campus/index, which obviously isn't a component. Most likely, the issue arose because the timing of the plugin I was using didn't align with the original plugin. The uniapp plugin was already completed in the pre-analysis phase, which was quite awkward. I thought of solving this json issue by writing a webpack plugin.
Through this plugin, I did manage to solve the problem of importing components from the json file successfully. I then started the WeChat Developer Tool and found that the component loaded successfully, but all the logic and styles were lost. It was strange. I checked the compilation of the component and found that the component was not compiled successfully at all. Both the js and css failed to compile. This was embarrassing. In fact, during the compilation process, the uniapp plugin did not throw any exceptions. It internally handled all the related issues without any indication. I still wanted to solve this problem by writing a webpack plugin. I tried handling it in the compiler and compilation hooks, but it didn't solve the problem. Later, while printing the NormalModuleFactory hook, I found that the source had been correctly specified as the desired path through the processing of babel-plugin-component. However, there were still problems during uniapp compilation. Then I started thinking about how early uniapp actually handles these things. I also tried the JavascriptParser hook, but it didn't handle it successfully. In fact, there is a plugin @dcloudio/webpack-uni-mp-loader/lib/babel/util.js which handles this matter. There were more pitfalls here.
Then I went back to the babel-plugin-import, as this is a handling plugin carried in the dependencies of uniapp. So theoretically, it is used in there. I noticed that there is a statement to handle @dcloudio/uni-ui in the babel.config.js.
So, I thought of writing something similar. The specific process is just a description. First, I had written a similar declaration before, but it did not take effect. I tried adding my components to process.UNI_LIBRARIES and found that it actually worked. This surprised me. I thought that there must be some processing in process.UNI_LIBRARIES, so I modified it slightly. After handling in process.UNI_LIBRARIES, the babel-plugin-import plugin also handled it. Then I started the compilation and found that the same problem still existed. The files could not be compiled successfully, the content was empty, and all the error information was hidden, without any error message coming out. This was really frustrating. It also affected the reference to the @dcloudio/uni-ui component. I found this by casually referencing a component, and found that the component here also became empty and could not be resolved successfully. In the json file, the declaration of my file in src/components was declared as being in the lib directory, and then I saw that there is a babel plugin that references @dcloudio/webpack-uni-mp-loader/lib/babel/util.js, and the processing of process.UNI_LIBRARIES in there was hardcoded. This really made it a nightmare. Therefore, to solve this problem, I must handle the reference declaration of the vue file in advance, because it is not a problem to directly declare the reference under src/components. If I want to handle this problem before uniapp processes it, then I can only write a loader to handle it. I implemented a regex to match the import statement and then parsed the import statement to process the complete path. Considering the complexity of references, I still considered using a relatively common parsing library to implement the parsing of the import statement rather than completing this task only through the matching of regular expressions. Then I used parse-imports to complete this loader.
// vue.config.jsconst path =require("path");consttransform=str=> str.replace(/\B([A-Z])/g,"-$1").toLowerCase();module.exports=function(source){const name =this.query.name;if(!name)return source;const path =this.query.path ||"lib";const main =this.query.main;return source.replace(// maybe use parse-imports to parse import statementnewRegExp(`import[\\s]*?\\{[\\s]*?([\\s\\S]*?)[\\s]*?\\}[\\s]*?from[\\s]*?[""]${name}[""];?`,"g"),function(_, $1){let target =""; $1.split(",").forEach(item=>{const transformedComponentName =transform(item.split("as")[0].trim());const single =`import { ${item} } from "${name}/${path}/${transformedComponentName}/${ main || transformedComponentName
}";`; target = target + single;});return target;});};module.exports ={transpileDependencies:["shst-campus"],configureWebpack:{resolve:{alias:{"@": path.join(__dirname,"./src"),},},module:{rules:[{test:/\.vue$/,loader:"shst-campus/build/components-loader",options:{name:"shst-campus",path:"src/components",main:"index",},},],},plugins:[],},};
Lately, I haven't had much to do, so I rewrote the loader mentioned earlier. If you use on-demand loading, you can ignore the above. Just install the dependencies and configure them in vue.config.js. For detailed configuration, you can check https://github.com/SHST-SDUST/SHST-PLUS/blob/master/vue.config.js.
$ yarnadd -D uniapp-import-loader
module.exports ={configureWebpack:{// ...module:{rules:[{test:/\.vue$/,loader:"uniapp-import-loader",// import { CCard } from "shst-campus"; // => import CCard from "shst-campus/lib/c-card/c-card";options:{name:"shst-campus",path:"lib",},},],},// ..},};
Recently, I've been studying the related code and the babel processing solution in the uniapp framework, and I've implemented a solution for on-demand importing using babel-plugin. You can choose between this solution and the webpack-loader solution. To implement this, you need to configure the babel.config.js. You can find detailed configuration at https://github.com/SHST-SDUST/SHST-PLUS/blob/master/babel.config.js.
$ yarnadd -D uniapp-import-loader
// ...process.UNI_LIBRARIES=["shst-campus"];plugins.push([require("uniapp-import-loader/dist/babel-plugin-dynamic-import"),{libraryName:"shst-campus",libraryPath:"lib",},// import { CCard } from "shst-campus";// => import CCard from "shst-campus/lib/c-card/c-card";]);// ...
Finally, it's time to prepare for the chosen solution. This solution is essentially the same as the usage method for @dcloudio/uni-ui, because since uniapp is hardcoded, we should adapt to this approach. We won't be doing any special handling of plugins using loaders or plugins. We'll just consider it as a standard. I've encountered too many obstacles, and I can't cope with them anymore. I actually think that using a loader to solve this problem is also acceptable, but in reality, the changes required are too extensive and need to be adapted universally. It's better to use a relatively universal approach. We can write a script to handle the structure of its components, which can be automatically built and published after completion. Now, I've generated an index.js in dist/package as the import main, as well as an index.d.ts as the declaration file, a README.md, package.json, .npmrc files, and the components conforming to the above directory structure. These are mainly file operations and writing the build and publish commands in package.json. You can compare the file differences between https://npm.runkit.com/shst-campus and https://github.com/WindrunnerMax/Campus, or just run npm run build:package at https://github.com/WindrunnerMax/Campus to see the npm package to be published in dist/package.
I originally thought this approach would be enough, but then ran into a major issue. The problem this time is that when using the on-demand import method, for example, import { CCard } from "shst-campus";, if the pages written in the local src directory use the decorator syntax, the components in the node_modules cannot be compiled properly. Whether the components in node_modules are in TS or regular vue format, the same problem occurs. This issue was discussed in detail in the blog post above; it's a major pitfall. The output of the compilation lacks CSS and JavaScript files, and only contains a Component({}). However, if the Vue.extend syntax is used, the components in node_modules can be compiled properly. Of course, locally written components in src without using TS will not have any issues. There are now three possible solutions, but the ultimate solution would be to write a webpack loader. I have implemented this in the blog, but decided not to use it due to its lack of versatility. If things get really tough, I will refine the loader. As for why a loader was written instead of just using a plugin, that can also be found in the blog. What a pitfall!
Components in src are written using the decorator syntax, and components are imported using the actual path, as in import CCard from "shst-campus/lib/c-card/c-card.vue";.
Components in src are written using the Vue.extend syntax, and can be imported on demand, as in import { CCard } from "shst-campus";.
Components in src can be written in either of these ways, and then configure the easycom provided by uniapp to use the components directly without declaring them.
If configuring on-demand import of components, as in import { CCard } from "shst-campus";, modification of the babel.config.js file is required.
This is the ultimate solution. Eventually, I took some time to use the parse-imports library to create a new loader. Compatibility should be good. Besides, this library is quite tricky. It's a module without being packaged as commonjs, which means that as a loader, I had to bundle all the dependencies into one js file, which is quite frustrating. I'm planning to use this approach to solve the issues with uniapp components, and also to test the library's compatibility. If you want to use the on-demand loading method, you can ignore the above. Just install the dependencies and configure them in the vue.config.js.
$ yarnadd -D uniapp-import-loader
// vue.config.jsconst path =require("path");module.exports ={configureWebpack:{// ...module:{rules:[{test:/\.vue$/,loader:"uniapp-import-loader",// import { CCard } from "shst-campus";// => import CCard from "shst-campus/lib/c-card/c-card";options:{name:"shst-campus",path:"lib",},},],},// ...},};